diff --git a/packages/sdk-core/src/BacktraceCoreClient.ts b/packages/sdk-core/src/BacktraceCoreClient.ts index 2416b1a8..5bc2076a 100644 --- a/packages/sdk-core/src/BacktraceCoreClient.ts +++ b/packages/sdk-core/src/BacktraceCoreClient.ts @@ -9,9 +9,12 @@ import { BacktraceReportSubmissionResult, BacktraceRequestHandler, BacktraceSessionProvider, + BacktraceStorageModule, BacktraceSubmissionResponse, DebugIdProvider, - FileSystem, + DefaultReportBacktraceDatabaseRecordFactory, + ReportBacktraceDatabaseRecordSender, + ReportBacktraceDatabaseRecordSerializer, SdkOptions, SessionFiles, } from './index.js'; @@ -117,7 +120,7 @@ export abstract class BacktraceCoreClient< protected readonly attributeManager: AttributeManager; protected readonly attachmentManager: AttachmentManager; - protected readonly fileSystem?: FileSystem; + protected readonly databaseStorage?: BacktraceStorageModule; private readonly _modules: BacktraceModules = new Map(); private readonly _dataBuilder: BacktraceDataBuilder; @@ -133,7 +136,6 @@ export abstract class BacktraceCoreClient< super(); this.options = setup.options; - this.fileSystem = setup.fileSystem; this._sdkOptions = setup.sdkOptions; this._sessionProvider = setup.sessionProvider ?? new SingleSessionProvider(); this._reportSubmission = @@ -167,31 +169,40 @@ export abstract class BacktraceCoreClient< new DebugIdProvider(stackTraceConverter, setup.debugIdMapProvider), ); - if (this.options?.database?.enable === true && setup.fileSystem) { + if (this.options?.database?.enable === true && setup.database?.storage) { + this.databaseStorage = setup.database.storage; + this.addModule(this.databaseStorage); + const provider = BacktraceDatabaseFileStorageProvider.createIfValid( - setup.fileSystem, + { + report: new ReportBacktraceDatabaseRecordSerializer(), + ...setup.database?.recordSerializers, + }, + this.databaseStorage, this.options.database, ); - if (this.fileSystem) { - const sessionFiles = new SessionFiles( - this.fileSystem, - this.options.database.path, - { - id: this.sessionId, - timestamp: Date.now(), - }, - this.options.database.maximumOldSessions ?? 1, - ); - this._modules.set(SessionFiles, sessionFiles); - } + const sessionFiles = new SessionFiles( + this.databaseStorage, + { + id: this.sessionId, + timestamp: Date.now(), + }, + this.options.database.maximumOldSessions ?? 1, + ); + this._modules.set(SessionFiles, sessionFiles); if (provider) { const database = new BacktraceDatabase( this.options.database, provider, - this._reportSubmission, + { + report: new ReportBacktraceDatabaseRecordSender(this._reportSubmission), + ...setup.database?.recordSenders?.(this._reportSubmission), + }, + setup.database?.reportRecordFactory ?? DefaultReportBacktraceDatabaseRecordFactory.default(), this.sessionFiles, + setup.database.recordLimits, ); this._modules.set(BacktraceDatabase, database); } @@ -223,16 +234,6 @@ export abstract class BacktraceCoreClient< public initialize() { this.validateAttributes(); - if (this.fileSystem && this.options.database?.createDatabaseDirectory) { - if (!this.options.database.path) { - throw new Error( - 'Missing mandatory path to the database. Please define the database.path option in the configuration.', - ); - } - - this.fileSystem.createDirSync(this.options.database?.path); - } - for (const module of this._modules.values()) { if (module.bind) { module.bind(this.getModuleBindData()); @@ -406,7 +407,7 @@ export abstract class BacktraceCoreClient< return data instanceof BacktraceReport; } - private getModuleBindData(): BacktraceModuleBindData { + private getModuleBindData(): BacktraceModuleBindData { return { client: this, options: this.options, @@ -416,7 +417,7 @@ export abstract class BacktraceCoreClient< requestHandler: this._requestHandler, database: this.database, sessionFiles: this.sessionFiles, - fileSystem: this.fileSystem, + storage: this.databaseStorage, }; } diff --git a/packages/sdk-core/src/builder/BacktraceCoreClientBuilder.ts b/packages/sdk-core/src/builder/BacktraceCoreClientBuilder.ts index cf81b4f4..a6d33144 100644 --- a/packages/sdk-core/src/builder/BacktraceCoreClientBuilder.ts +++ b/packages/sdk-core/src/builder/BacktraceCoreClientBuilder.ts @@ -12,7 +12,7 @@ import { BacktraceSessionProvider } from '../modules/metrics/BacktraceSessionPro import { MetricsQueue } from '../modules/metrics/MetricsQueue.js'; import { SummedEvent } from '../modules/metrics/model/SummedEvent.js'; import { UniqueEvent } from '../modules/metrics/model/UniqueEvent.js'; -import { FileSystem } from '../modules/storage/index.js'; +import { BacktraceStorageModule } from '../modules/storage/index.js'; import { CoreClientSetup } from './CoreClientSetup.js'; type Writeable = { -readonly [P in keyof T]: T[P] }; @@ -77,8 +77,11 @@ export abstract class BacktraceCoreClientBuilder; readonly uniqueMetricsQueue?: MetricsQueue; + readonly database?: { + readonly storage?: BacktraceStorageModule; + readonly recordSerializers?: BacktraceDatabaseRecordSerializers; + readonly recordSenders?: (submission: BacktraceReportSubmission) => BacktraceDatabaseRecordSenders; + readonly reportRecordFactory?: ReportBacktraceDatabaseRecordFactory; + readonly recordLimits?: BacktraceDatabaseRecordCountByType; + }; } diff --git a/packages/sdk-core/src/common/asyncGenerator.ts b/packages/sdk-core/src/common/asyncGenerator.ts new file mode 100644 index 00000000..4aee5f33 --- /dev/null +++ b/packages/sdk-core/src/common/asyncGenerator.ts @@ -0,0 +1,7 @@ +export async function toArray(generator: AsyncGenerator): Promise { + const result: T[] = []; + for await (const element of generator) { + result.push(element); + } + return result; +} diff --git a/packages/sdk-core/src/model/attachment/BacktraceFileAttachment.ts b/packages/sdk-core/src/model/attachment/BacktraceFileAttachment.ts deleted file mode 100644 index e4b15cea..00000000 --- a/packages/sdk-core/src/model/attachment/BacktraceFileAttachment.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { BacktraceAttachment } from './BacktraceAttachment.js'; - -export interface BacktraceFileAttachment extends BacktraceAttachment { - /** - * File path to attachment. - */ - readonly filePath: string; -} diff --git a/packages/sdk-core/src/model/attachment/index.ts b/packages/sdk-core/src/model/attachment/index.ts index 3d55accb..f562173b 100644 --- a/packages/sdk-core/src/model/attachment/index.ts +++ b/packages/sdk-core/src/model/attachment/index.ts @@ -1,4 +1,3 @@ export * from './BacktraceAttachment.js'; -export * from './BacktraceFileAttachment.js'; export * from './BacktraceStringAttachment.js'; export * from './BacktraceUint8ArrayAttachment.js'; diff --git a/packages/sdk-core/src/model/configuration/BacktraceDatabaseConfiguration.ts b/packages/sdk-core/src/model/configuration/BacktraceDatabaseConfiguration.ts index 289a6a2d..4fabe1d7 100644 --- a/packages/sdk-core/src/model/configuration/BacktraceDatabaseConfiguration.ts +++ b/packages/sdk-core/src/model/configuration/BacktraceDatabaseConfiguration.ts @@ -3,15 +3,6 @@ export interface EnabledBacktraceDatabaseConfiguration { * Determine if the Database is enabled */ enable: true; - /** - * Path where the SDK can store data. - */ - path: string; - /** - * Determine if the directory should be auto created by the SDK. - * @default true - */ - createDatabaseDirectory?: boolean; /** * Sends reports to the server based on the retry settings. @@ -26,13 +17,6 @@ export interface EnabledBacktraceDatabaseConfiguration { */ maximumNumberOfRecords?: number; - /** - * The maximum number of attachments stored in the offline database. When the limit is reached, - * the oldest attachments are removed. If the value is equal to '0', then no limit is set. - * @default 10 - */ - maximumNumberOfAttachmentRecords?: number; - /** * The amount of time (in ms) to wait between retries if the database is unable to send a report. * The default value is 60 000 diff --git a/packages/sdk-core/src/modules/BacktraceModule.ts b/packages/sdk-core/src/modules/BacktraceModule.ts index 9b16144a..9814e143 100644 --- a/packages/sdk-core/src/modules/BacktraceModule.ts +++ b/packages/sdk-core/src/modules/BacktraceModule.ts @@ -2,7 +2,7 @@ import { BacktraceConfiguration, BacktraceCoreClient, BacktraceRequestHandler, - FileSystem, + BacktraceStorageModule, SessionFiles, } from '../index.js'; import { BacktraceReportSubmission } from '../model/http/BacktraceReportSubmission.js'; @@ -10,20 +10,20 @@ import { AttachmentManager } from './attachments/AttachmentManager.js'; import { AttributeManager } from './attribute/AttributeManager.js'; import { BacktraceDatabase } from './database/BacktraceDatabase.js'; -export interface BacktraceModuleBindData { +export interface BacktraceModuleBindData { readonly client: BacktraceCoreClient; - readonly options: BacktraceConfiguration; + readonly options: O; readonly attributeManager: AttributeManager; readonly attachmentManager: AttachmentManager; readonly reportSubmission: BacktraceReportSubmission; readonly requestHandler: BacktraceRequestHandler; readonly database?: BacktraceDatabase; readonly sessionFiles?: SessionFiles; - readonly fileSystem?: FileSystem; + readonly storage?: BacktraceStorageModule; } -export interface BacktraceModule { - bind?(client: BacktraceModuleBindData): void; +export interface BacktraceModule { + bind?(client: BacktraceModuleBindData): void; initialize?(): void; dispose?(): void; } diff --git a/packages/sdk-core/src/modules/attachments/isFileAttachment.ts b/packages/sdk-core/src/modules/attachments/isFileAttachment.ts deleted file mode 100644 index 83bece99..00000000 --- a/packages/sdk-core/src/modules/attachments/isFileAttachment.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { BacktraceAttachment, BacktraceFileAttachment } from '../../model/attachment/index.js'; - -export function isFileAttachment(attachment: BacktraceAttachment): attachment is BacktraceFileAttachment { - return 'filePath' in attachment && typeof attachment.filePath === 'string'; -} diff --git a/packages/sdk-core/src/modules/attribute/FileAttributeManager.ts b/packages/sdk-core/src/modules/attribute/FileAttributeManager.ts index 905ee34d..b61d3b29 100644 --- a/packages/sdk-core/src/modules/attribute/FileAttributeManager.ts +++ b/packages/sdk-core/src/modules/attribute/FileAttributeManager.ts @@ -1,7 +1,8 @@ import { jsonEscaper } from '../../common/jsonEscaper.js'; import { AttributeType } from '../../model/data/index.js'; import { BacktraceModule, BacktraceModuleBindData } from '../BacktraceModule.js'; -import { FileSystem, SessionFiles } from '../storage/index.js'; +import { BacktraceStorage, BacktraceSyncStorage } from '../storage/BacktraceStorage.js'; +import { SessionFiles } from '../storage/index.js'; import { AttributeManager } from './AttributeManager.js'; const ATTRIBUTE_FILE_NAME = 'bt-attributes'; @@ -10,17 +11,17 @@ export class FileAttributeManager implements BacktraceModule { private _attributeManager?: AttributeManager; constructor( - private readonly _fileSystem: FileSystem, + private readonly _storage: BacktraceStorage, private _fileName?: string, ) {} - public static create(fileSystem: FileSystem) { - return new FileAttributeManager(fileSystem); + public static create(storage: BacktraceStorage) { + return new FileAttributeManager(storage); } - public static createFromSession(sessionFiles: SessionFiles, fileSystem: FileSystem) { + public static createFromSession(sessionFiles: SessionFiles, storage: BacktraceStorage & BacktraceSyncStorage) { const fileName = sessionFiles.getFileName(ATTRIBUTE_FILE_NAME); - return new FileAttributeManager(fileSystem, fileName); + return new FileAttributeManager(storage, fileName); } public initialize(): void { @@ -51,7 +52,10 @@ export class FileAttributeManager implements BacktraceModule { } try { - const content = await this._fileSystem.readFile(this._fileName); + const content = await this._storage.get(this._fileName); + if (!content) { + return {}; + } return JSON.parse(content); } catch { return {}; @@ -64,6 +68,6 @@ export class FileAttributeManager implements BacktraceModule { } const reportData = this._attributeManager.get('scoped'); - await this._fileSystem.writeFile(this._fileName, JSON.stringify(reportData.attributes, jsonEscaper())); + await this._storage.set(this._fileName, JSON.stringify(reportData.attributes, jsonEscaper())); } } diff --git a/packages/sdk-core/src/modules/database/AttachmentBacktraceDatabaseFileRecord.ts b/packages/sdk-core/src/modules/database/AttachmentBacktraceDatabaseFileRecord.ts deleted file mode 100644 index a990b683..00000000 --- a/packages/sdk-core/src/modules/database/AttachmentBacktraceDatabaseFileRecord.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { BacktraceAttachment } from '../../model/attachment/index.js'; -import { isFileAttachment } from '../attachments/isFileAttachment.js'; -import { FileSystem, SessionId } from '../storage/index.js'; -import { BacktraceDatabaseFileRecord } from './BacktraceDatabaseFileRecord.js'; -import { AttachmentBacktraceDatabaseRecord } from './model/BacktraceDatabaseRecord.js'; - -export class AttachmentBacktraceDatabaseFileRecord implements AttachmentBacktraceDatabaseRecord { - public readonly type = 'attachment'; - public readonly id: string; - public readonly rxid: string; - public readonly timestamp: number; - public readonly attachment: BacktraceAttachment; - public readonly sessionId: SessionId; - public locked: boolean; - - private constructor(record: AttachmentBacktraceDatabaseRecord) { - this.attachment = record.attachment; - this.id = record.id; - this.timestamp = record.timestamp; - this.rxid = record.rxid; - this.sessionId = record.sessionId; - // make sure the database record stored in the database directory - // is never locked. By doing this, we want to be sure once we load - // the record once again, the record will be available for future usage - this.locked = false; - } - - public static fromRecord(record: AttachmentBacktraceDatabaseRecord) { - return new AttachmentBacktraceDatabaseFileRecord(record); - } - - public static fromJson( - json: string | AttachmentBacktraceDatabaseRecord, - fileSystem: FileSystem, - ): BacktraceDatabaseFileRecord | undefined { - try { - const record = typeof json === 'string' ? (JSON.parse(json) as AttachmentBacktraceDatabaseRecord) : json; - if (!isFileAttachment(record.attachment)) { - return undefined; - } - - if (!fileSystem.existsSync(record.attachment.filePath)) { - return undefined; - } - - const attachment = fileSystem.createAttachment(record.attachment.filePath, record.attachment.name); - return new AttachmentBacktraceDatabaseFileRecord({ - ...record, - attachment, - }); - } catch { - return undefined; - } - } -} diff --git a/packages/sdk-core/src/modules/database/BacktraceDatabase.ts b/packages/sdk-core/src/modules/database/BacktraceDatabase.ts index 3b0d3d29..ac3924d6 100644 --- a/packages/sdk-core/src/modules/database/BacktraceDatabase.ts +++ b/packages/sdk-core/src/modules/database/BacktraceDatabase.ts @@ -1,23 +1,19 @@ import { anySignal, createAbortController } from '../../common/AbortController.js'; import { Events } from '../../common/Events.js'; -import { IdGenerator } from '../../common/IdGenerator.js'; import { unrefInterval } from '../../common/intervalHelper.js'; import { TimeHelper } from '../../common/TimeHelper.js'; -import { BacktraceAttachment } from '../../model/attachment/index.js'; +import { BacktraceAttachment } from '../../model/attachment/BacktraceAttachment.js'; import { BacktraceDatabaseConfiguration } from '../../model/configuration/BacktraceDatabaseConfiguration.js'; import { BacktraceData } from '../../model/data/BacktraceData.js'; -import { BacktraceReportSubmission } from '../../model/http/BacktraceReportSubmission.js'; import { BacktraceModule, BacktraceModuleBindData } from '../BacktraceModule.js'; -import { SessionFiles, SessionId } from '../storage/index.js'; +import { SessionFiles } from '../storage/index.js'; import { BacktraceDatabaseContext } from './BacktraceDatabaseContext.js'; import { BacktraceDatabaseEvents } from './BacktraceDatabaseEvents.js'; +import { BacktraceDatabaseRecordSenders } from './BacktraceDatabaseRecordSender.js'; import { BacktraceDatabaseStorageProvider } from './BacktraceDatabaseStorageProvider.js'; -import { - AttachmentBacktraceDatabaseRecord, - BacktraceDatabaseRecord, - BacktraceDatabaseRecordCountByType, - ReportBacktraceDatabaseRecord, -} from './model/BacktraceDatabaseRecord.js'; +import { BacktraceDatabaseRecord, BacktraceDatabaseRecordCountByType } from './model/BacktraceDatabaseRecord.js'; +import { ReportBacktraceDatabaseRecord } from './model/ReportBacktraceDatabaseRecord.js'; +import { ReportBacktraceDatabaseRecordFactory } from './ReportBacktraceDatabaseRecordFactory.js'; export class BacktraceDatabase extends Events implements BacktraceModule { /** @@ -45,15 +41,17 @@ export class BacktraceDatabase extends Events implement constructor( private readonly _options: BacktraceDatabaseConfiguration | undefined, private readonly _storageProvider: BacktraceDatabaseStorageProvider, - private readonly _requestHandler: BacktraceReportSubmission, + private readonly _recordSenders: BacktraceDatabaseRecordSenders, + private readonly _reportRecordFactory: ReportBacktraceDatabaseRecordFactory, private readonly _sessionFiles?: SessionFiles, + recordLimits?: BacktraceDatabaseRecordCountByType, ) { super(); this._databaseRecordContext = new BacktraceDatabaseContext(this._options?.maximumRetries); this._recordLimits = { report: this._options?.maximumNumberOfRecords ?? 8, - attachment: this._options?.maximumNumberOfAttachmentRecords ?? 10, + ...recordLimits, }; this._retryInterval = this._options?.retryInterval ?? 60_000; } @@ -108,7 +106,8 @@ export class BacktraceDatabase extends Events implement client.on('after-send', (_, data, __, submissionResult) => { const record = this._databaseRecordContext.find( - (record) => record.type === 'report' && record.data.uuid === data.uuid, + (record) => + record.type === 'report' && (record as ReportBacktraceDatabaseRecord).data.uuid === data.uuid, ); if (!record) { return; @@ -135,56 +134,15 @@ export class BacktraceDatabase extends Events implement return undefined; } - const record: ReportBacktraceDatabaseRecord = { - type: 'report', - data: backtraceData, - timestamp: TimeHelper.now(), - id: IdGenerator.uuid(), - locked: false, - attachments: attachments, - sessionId: this._sessionFiles?.sessionId, - }; - - this.prepareDatabase([record]); - const saveResult = this._storageProvider.add(record); - if (!saveResult) { - return undefined; - } - - this._databaseRecordContext.add(record); - this.lockSessionWithRecord(record); - - this.emit('added', record); - - return record; + const reportRecord = this._reportRecordFactory.create( + backtraceData, + attachments, + this._sessionFiles?.sessionId, + ); + return this.addRecord(reportRecord); } - /** - * Adds Bactrace attachment to the database - * @param backtraceData diagnostic data object - * @param attachment the attachment to add - * @param sessionId session ID to use - * @returns record if database is enabled. Otherwise undefined. - */ - public addAttachment( - rxid: string, - attachment: BacktraceAttachment, - sessionId: SessionId, - ): AttachmentBacktraceDatabaseRecord | undefined { - if (!this._enabled) { - return undefined; - } - - const record: AttachmentBacktraceDatabaseRecord = { - type: 'attachment', - timestamp: TimeHelper.now(), - id: IdGenerator.uuid(), - rxid, - locked: false, - attachment, - sessionId, - }; - + public addRecord(record: R): R | undefined { this.prepareDatabase([record]); const saveResult = this._storageProvider.add(record); if (!saveResult) { @@ -296,12 +254,15 @@ export class BacktraceDatabase extends Events implement try { record.locked = true; + const sender = this._recordSenders[record.type]; + if (!sender) { + this.remove(record); + continue; + } + this.emit('before-send', record); - const result = - record.type === 'report' - ? await this._requestHandler.send(record.data, record.attachments, signal) - : await this._requestHandler.sendAttachment(record.rxid, record.attachment, signal); + const result = await sender.send(record, signal); this.emit('after-send', record, result); diff --git a/packages/sdk-core/src/modules/database/BacktraceDatabaseFileRecord.ts b/packages/sdk-core/src/modules/database/BacktraceDatabaseFileRecord.ts deleted file mode 100644 index 516a3232..00000000 --- a/packages/sdk-core/src/modules/database/BacktraceDatabaseFileRecord.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { FileSystem } from '../storage/index.js'; -import { AttachmentBacktraceDatabaseFileRecord } from './AttachmentBacktraceDatabaseFileRecord.js'; -import { BacktraceDatabaseRecord } from './model/BacktraceDatabaseRecord.js'; -import { ReportBacktraceDatabaseFileRecord } from './ReportBacktraceDatabaseFileRecord.js'; - -export const BacktraceDatabaseFileRecord = { - fromRecord(record: BacktraceDatabaseRecord) { - switch (record.type) { - case 'attachment': - return AttachmentBacktraceDatabaseFileRecord.fromRecord(record); - case 'report': - return ReportBacktraceDatabaseFileRecord.fromRecord(record); - default: - throw new Error('unknown record type'); - } - }, - - fromJson(json: string, fileSystem: FileSystem): BacktraceDatabaseFileRecord | undefined { - try { - const record = JSON.parse(json) as BacktraceDatabaseRecord; - switch (record.type) { - case 'attachment': - return AttachmentBacktraceDatabaseFileRecord.fromJson(record, fileSystem); - case 'report': - return ReportBacktraceDatabaseFileRecord.fromJson(record, fileSystem); - default: - return undefined; - } - } catch { - return undefined; - } - }, -}; - -export type BacktraceDatabaseFileRecord = ReportBacktraceDatabaseFileRecord | AttachmentBacktraceDatabaseFileRecord; diff --git a/packages/sdk-core/src/modules/database/BacktraceDatabaseFileStorageProvider.ts b/packages/sdk-core/src/modules/database/BacktraceDatabaseFileStorageProvider.ts index 9c4108e0..ba9344c2 100644 --- a/packages/sdk-core/src/modules/database/BacktraceDatabaseFileStorageProvider.ts +++ b/packages/sdk-core/src/modules/database/BacktraceDatabaseFileStorageProvider.ts @@ -1,7 +1,7 @@ -import { jsonEscaper } from '../../common/jsonEscaper.js'; +import { toArray } from '../../common/asyncGenerator.js'; import { BacktraceDatabaseConfiguration } from '../../model/configuration/BacktraceDatabaseConfiguration.js'; -import { FileSystem } from '../storage/index.js'; -import { BacktraceDatabaseFileRecord } from './BacktraceDatabaseFileRecord.js'; +import { BacktraceIterableStorage, BacktraceStorage, BacktraceSyncStorage } from '../storage/BacktraceStorage.js'; +import { BacktraceDatabaseRecordSerializers } from './BacktraceDatabaseRecordSerializer.js'; import { BacktraceDatabaseStorageProvider } from './BacktraceDatabaseStorageProvider.js'; import { BacktraceDatabaseRecord } from './model/BacktraceDatabaseRecord.js'; @@ -10,8 +10,8 @@ export class BacktraceDatabaseFileStorageProvider implements BacktraceDatabaseSt private readonly RECORD_SUFFIX = '-record.json'; private constructor( - private readonly _fileSystem: FileSystem, - private readonly _path: string, + private readonly _serializers: BacktraceDatabaseRecordSerializers, + private readonly _storage: BacktraceSyncStorage & BacktraceStorage & BacktraceIterableStorage, ) {} /** @@ -20,7 +20,8 @@ export class BacktraceDatabaseFileStorageProvider implements BacktraceDatabaseSt * @returns database file storage provider */ public static createIfValid( - fileSystem: FileSystem, + serializers: BacktraceDatabaseRecordSerializers, + storage: BacktraceSyncStorage & BacktraceStorage & BacktraceIterableStorage, options?: BacktraceDatabaseConfiguration, ): BacktraceDatabaseFileStorageProvider | undefined { if (!options) { @@ -31,13 +32,7 @@ export class BacktraceDatabaseFileStorageProvider implements BacktraceDatabaseSt return undefined; } - if (options.enable && !options.path) { - throw new Error( - 'Missing mandatory path to the database. Please define the database.path option in the configuration.', - ); - } - - return new BacktraceDatabaseFileStorageProvider(fileSystem, options.path); + return new BacktraceDatabaseFileStorageProvider(serializers, storage); } public start(): boolean { @@ -56,11 +51,18 @@ export class BacktraceDatabaseFileStorageProvider implements BacktraceDatabaseSt public add(record: BacktraceDatabaseRecord): boolean { const recordPath = this.getRecordPath(record.id); + const serializer = this._serializers[record.type]; + if (!serializer) { + return false; + } + try { - this._fileSystem.writeFileSync( - recordPath, - JSON.stringify(BacktraceDatabaseFileRecord.fromRecord(record), jsonEscaper()), - ); + const serialized = serializer.save(record); + if (!serialized) { + return false; + } + + this._storage.setSync(recordPath, serialized); return true; } catch { return false; @@ -68,24 +70,35 @@ export class BacktraceDatabaseFileStorageProvider implements BacktraceDatabaseSt } public async get(): Promise { - const databaseFiles = await this._fileSystem.readDir(this._path); + const databaseFiles = await toArray(this._storage.keys()); - const recordNames = databaseFiles - .filter((file) => file.endsWith(this.RECORD_SUFFIX)) - .map((f) => this._path + '/' + f); + const recordNames = databaseFiles.filter((file) => file.endsWith(this.RECORD_SUFFIX)); const records: BacktraceDatabaseRecord[] = []; for (const recordName of recordNames) { try { - const recordJson = await this._fileSystem.readFile(recordName); - const record = BacktraceDatabaseFileRecord.fromJson(recordJson, this._fileSystem); + const recordJson = await this._storage.get(recordName); + if (!recordJson) { + await this._storage.remove(recordName); + continue; + } + + let record: BacktraceDatabaseRecord | undefined; + for (const serializer of Object.values(this._serializers)) { + record = serializer.load(recordJson); + if (record) { + break; + } + } + if (!record) { - await this._fileSystem.unlink(recordName); + await this._storage.remove(recordName); continue; } + records.push(record); } catch { - await this._fileSystem.unlink(recordName); + await this._storage.remove(recordName); } } @@ -93,12 +106,12 @@ export class BacktraceDatabaseFileStorageProvider implements BacktraceDatabaseSt } private unlinkRecord(recordPath: string): boolean { - if (!this._fileSystem.existsSync(recordPath)) { + if (!this._storage.hasSync(recordPath)) { return false; } try { - this._fileSystem.unlinkSync(recordPath); + this._storage.removeSync(recordPath); return true; } catch { return false; @@ -106,6 +119,6 @@ export class BacktraceDatabaseFileStorageProvider implements BacktraceDatabaseSt } private getRecordPath(id: string): string { - return this._path + '/' + `${id}${this.RECORD_SUFFIX}`; + return `${id}${this.RECORD_SUFFIX}`; } } diff --git a/packages/sdk-core/src/modules/database/BacktraceDatabaseRecordSender.ts b/packages/sdk-core/src/modules/database/BacktraceDatabaseRecordSender.ts new file mode 100644 index 00000000..4ffbca88 --- /dev/null +++ b/packages/sdk-core/src/modules/database/BacktraceDatabaseRecordSender.ts @@ -0,0 +1,13 @@ +import { BacktraceReportSubmissionResult } from '../../model/data/BacktraceSubmissionResult.js'; +import { BacktraceSubmitResponse } from '../../model/http/index.js'; +import { BacktraceDatabaseRecord } from './model/BacktraceDatabaseRecord.js'; + +export interface BacktraceDatabaseRecordSender { + readonly type: Record['type']; + + send(record: Record, abortSignal?: AbortSignal): Promise>; +} + +export type BacktraceDatabaseRecordSenders = { + [R in Record as Record['type']]: BacktraceDatabaseRecordSender; +}; diff --git a/packages/sdk-core/src/modules/database/BacktraceDatabaseRecordSerializer.ts b/packages/sdk-core/src/modules/database/BacktraceDatabaseRecordSerializer.ts new file mode 100644 index 00000000..53291d4f --- /dev/null +++ b/packages/sdk-core/src/modules/database/BacktraceDatabaseRecordSerializer.ts @@ -0,0 +1,11 @@ +import { BacktraceDatabaseRecord } from './model/BacktraceDatabaseRecord.js'; + +export interface BacktraceDatabaseRecordSerializer { + readonly type: Record['type']; + save(record: Record): string | undefined; + load(serialized: string): Record | undefined; +} + +export type BacktraceDatabaseRecordSerializers = { + [R in Record as Record['type']]: BacktraceDatabaseRecordSerializer; +}; diff --git a/packages/sdk-core/src/modules/database/ReportBacktraceDatabaseFileRecord.ts b/packages/sdk-core/src/modules/database/ReportBacktraceDatabaseFileRecord.ts deleted file mode 100644 index 116c5fcf..00000000 --- a/packages/sdk-core/src/modules/database/ReportBacktraceDatabaseFileRecord.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { BacktraceAttachment } from '../../model/attachment/index.js'; -import { BacktraceData } from '../../model/data/index.js'; -import { isFileAttachment } from '../attachments/isFileAttachment.js'; -import { FileSystem, SessionId } from '../storage/index.js'; -import { BacktraceDatabaseFileRecord } from './BacktraceDatabaseFileRecord.js'; -import { ReportBacktraceDatabaseRecord } from './model/BacktraceDatabaseRecord.js'; - -export class ReportBacktraceDatabaseFileRecord implements ReportBacktraceDatabaseRecord { - public readonly type = 'report'; - public readonly data: BacktraceData; - public readonly id: string; - public readonly timestamp: number; - public readonly sessionId?: SessionId; - public locked: boolean; - - private constructor( - record: ReportBacktraceDatabaseRecord, - public readonly attachments: BacktraceAttachment[], - ) { - this.data = record.data; - this.id = record.id; - this.timestamp = record.timestamp; - this.sessionId = record.sessionId; - // make sure the database record stored in the database directory - // is never locked. By doing this, we want to be sure once we load - // the record once again, the record will be available for future usage - this.locked = false; - } - - public static fromRecord(record: ReportBacktraceDatabaseRecord) { - return new ReportBacktraceDatabaseFileRecord(record, record.attachments.filter(isFileAttachment)); - } - - public static fromJson( - json: string | ReportBacktraceDatabaseRecord, - fileSystem: FileSystem, - ): BacktraceDatabaseFileRecord | undefined { - try { - const record = typeof json === 'string' ? (JSON.parse(json) as ReportBacktraceDatabaseRecord) : json; - const attachments = record.attachments - ? record.attachments - .filter(isFileAttachment) - .map((n) => fileSystem.createAttachment(n.filePath, n.name)) - : []; - return new ReportBacktraceDatabaseFileRecord(record, attachments); - } catch { - return undefined; - } - } -} diff --git a/packages/sdk-core/src/modules/database/ReportBacktraceDatabaseRecordFactory.ts b/packages/sdk-core/src/modules/database/ReportBacktraceDatabaseRecordFactory.ts new file mode 100644 index 00000000..62391eaa --- /dev/null +++ b/packages/sdk-core/src/modules/database/ReportBacktraceDatabaseRecordFactory.ts @@ -0,0 +1,12 @@ +import { BacktraceAttachment } from '../../model/attachment/BacktraceAttachment.js'; +import { BacktraceData } from '../../model/data/BacktraceData.js'; +import { SessionId } from '../storage/SessionFiles.js'; +import { ReportBacktraceDatabaseRecord } from './model/ReportBacktraceDatabaseRecord.js'; + +export interface ReportBacktraceDatabaseRecordFactory { + create( + data: BacktraceData, + attachments: BacktraceAttachment[], + sessionId?: SessionId, + ): ReportBacktraceDatabaseRecord; +} diff --git a/packages/sdk-core/src/modules/database/index.ts b/packages/sdk-core/src/modules/database/index.ts index 6022c99c..dfaa35fb 100644 --- a/packages/sdk-core/src/modules/database/index.ts +++ b/packages/sdk-core/src/modules/database/index.ts @@ -1,3 +1,5 @@ export { BacktraceDatabase } from './BacktraceDatabase.js'; export * from './BacktraceDatabaseStorageProvider.js'; export * from './model/BacktraceDatabaseRecord.js'; +export * from './model/ReportBacktraceDatabaseRecord.js'; +export * from './ReportBacktraceDatabaseRecordFactory.js'; diff --git a/packages/sdk-core/src/modules/database/model/BacktraceDatabaseRecord.ts b/packages/sdk-core/src/modules/database/model/BacktraceDatabaseRecord.ts index f06614d9..80535229 100644 --- a/packages/sdk-core/src/modules/database/model/BacktraceDatabaseRecord.ts +++ b/packages/sdk-core/src/modules/database/model/BacktraceDatabaseRecord.ts @@ -1,33 +1,27 @@ -import { BacktraceAttachment } from '../../../model/attachment/index.js'; -import { BacktraceData } from '../../../model/data/BacktraceData.js'; +import { IdGenerator } from '../../../common/IdGenerator.js'; +import { TimeHelper } from '../../../common/TimeHelper.js'; import { SessionId } from '../../storage/SessionFiles.js'; -export interface ReportBacktraceDatabaseRecord { - readonly type: 'report'; - readonly data: BacktraceData; +export interface BacktraceDatabaseRecord { readonly id: string; + readonly type: Type; readonly timestamp: number; readonly sessionId?: SessionId; - attachments: BacktraceAttachment[]; /** * Determines if the record is in use */ locked: boolean; } -export interface AttachmentBacktraceDatabaseRecord { - readonly type: 'attachment'; - readonly id: string; - readonly rxid: string; - readonly timestamp: number; - readonly attachment: BacktraceAttachment; - readonly sessionId: SessionId; - /** - * Determines if the record is in use - */ - locked: boolean; +export class BacktraceDatabaseRecordFactory { + public create(type: Type): BacktraceDatabaseRecord { + return { + id: IdGenerator.uuid(), + timestamp: TimeHelper.now(), + type, + locked: false, + }; + } } -export type BacktraceDatabaseRecord = ReportBacktraceDatabaseRecord | AttachmentBacktraceDatabaseRecord; - export type BacktraceDatabaseRecordCountByType = Record; diff --git a/packages/sdk-core/src/modules/database/model/ReportBacktraceDatabaseRecord.ts b/packages/sdk-core/src/modules/database/model/ReportBacktraceDatabaseRecord.ts new file mode 100644 index 00000000..f290866a --- /dev/null +++ b/packages/sdk-core/src/modules/database/model/ReportBacktraceDatabaseRecord.ts @@ -0,0 +1,74 @@ +import { jsonEscaper } from '../../../common/jsonEscaper.js'; +import { BacktraceAttachment } from '../../../model/attachment/BacktraceAttachment.js'; +import { BacktraceData } from '../../../model/data/BacktraceData.js'; +import { BacktraceReportSubmission } from '../../../model/http/BacktraceReportSubmission.js'; +import { BacktraceReportSubmissionResult, BacktraceSubmitResponse } from '../../../model/http/index.js'; +import { SessionId } from '../../storage/SessionFiles.js'; +import { BacktraceDatabaseRecordSender } from '../BacktraceDatabaseRecordSender.js'; +import { BacktraceDatabaseRecordSerializer } from '../BacktraceDatabaseRecordSerializer.js'; +import { ReportBacktraceDatabaseRecordFactory } from '../ReportBacktraceDatabaseRecordFactory.js'; +import { BacktraceDatabaseRecord, BacktraceDatabaseRecordFactory } from './BacktraceDatabaseRecord.js'; + +export interface ReportBacktraceDatabaseRecord extends BacktraceDatabaseRecord<'report'> { + readonly data: BacktraceData; +} + +export class ReportBacktraceDatabaseRecordSerializer + implements BacktraceDatabaseRecordSerializer +{ + public readonly type = 'report'; + + public save(record: BacktraceDatabaseRecord): string { + return JSON.stringify(record, jsonEscaper()); + } + + public load(json: string): ReportBacktraceDatabaseRecord | undefined { + try { + const record = JSON.parse(json) as BacktraceDatabaseRecord; + if (record.type !== this.type) { + return undefined; + } + return record as ReportBacktraceDatabaseRecord; + } catch { + return undefined; + } + } +} + +export class ReportBacktraceDatabaseRecordSender + implements BacktraceDatabaseRecordSender +{ + public readonly type = 'report'; + + constructor(private readonly _reportSubmission: BacktraceReportSubmission) {} + + public send( + record: ReportBacktraceDatabaseRecord, + abortSignal?: AbortSignal, + ): Promise> { + return this._reportSubmission.send(record.data, [], abortSignal); + } +} + +export class DefaultReportBacktraceDatabaseRecordFactory implements ReportBacktraceDatabaseRecordFactory { + constructor(private readonly _recordFactory: BacktraceDatabaseRecordFactory) {} + + public static default() { + return new DefaultReportBacktraceDatabaseRecordFactory(new BacktraceDatabaseRecordFactory()); + } + + public create( + data: BacktraceData, + _attachments: BacktraceAttachment[], + sessionId?: SessionId, + ): ReportBacktraceDatabaseRecord { + const record: ReportBacktraceDatabaseRecord = { + ...this._recordFactory.create('report'), + sessionId, + data, + locked: false, + }; + + return record; + } +} diff --git a/packages/sdk-core/src/modules/storage/BacktraceStorage.ts b/packages/sdk-core/src/modules/storage/BacktraceStorage.ts new file mode 100644 index 00000000..bf1d3479 --- /dev/null +++ b/packages/sdk-core/src/modules/storage/BacktraceStorage.ts @@ -0,0 +1,36 @@ +import { BacktraceConfiguration } from '../../index.js'; +import { BacktraceModule } from '../BacktraceModule.js'; + +export interface BacktraceReadonlyStorage { + get(key: string): Promise; + has(key: string): Promise; +} + +export interface BacktraceIterableStorage { + keys(): AsyncGenerator; +} + +export interface BacktraceStorage extends BacktraceReadonlyStorage { + set(key: string, value: string): Promise; + remove(key: string): Promise; +} + +export interface BacktraceReadonlySyncStorage { + getSync(key: string): string | undefined; + hasSync(key: string): boolean; +} + +export interface BacktraceSyncStorage extends BacktraceReadonlySyncStorage { + setSync(key: string, value: string): boolean; + removeSync(key: string): boolean; +} + +export interface BacktraceIterableSyncStorage { + keysSync(): Generator; +} + +export type BacktraceStorageModule = BacktraceModule & + BacktraceStorage & + BacktraceSyncStorage & + BacktraceIterableStorage & + BacktraceIterableSyncStorage; diff --git a/packages/sdk-core/src/modules/storage/FileSystem.ts b/packages/sdk-core/src/modules/storage/FileSystem.ts deleted file mode 100644 index 70bb04a8..00000000 --- a/packages/sdk-core/src/modules/storage/FileSystem.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { BacktraceAttachment } from '../../model/attachment/index.js'; - -export interface FileSystem { - readDir(dir: string): Promise; - readDirSync(dir: string): string[]; - - createDir(dir: string): Promise; - createDirSync(dir: string): void; - - readFile(path: string): Promise; - readFileSync(path: string): string; - - writeFile(path: string, content: string): Promise; - writeFileSync(path: string, content: string): void; - - unlink(path: string): Promise; - unlinkSync(path: string): void; - - exists(path: string): Promise; - existsSync(path: string): boolean; - - createAttachment(path: string, name?: string): BacktraceAttachment; -} diff --git a/packages/sdk-core/src/modules/storage/SessionFiles.ts b/packages/sdk-core/src/modules/storage/SessionFiles.ts index ad6293e3..e4345c8b 100644 --- a/packages/sdk-core/src/modules/storage/SessionFiles.ts +++ b/packages/sdk-core/src/modules/storage/SessionFiles.ts @@ -1,7 +1,7 @@ import { Events } from '../../common/Events.js'; import { IdGenerator } from '../../common/IdGenerator.js'; import { BacktraceModule } from '../BacktraceModule.js'; -import { FileSystem } from './FileSystem.js'; +import { BacktraceIterableSyncStorage, BacktraceSyncStorage } from './BacktraceStorage.js'; interface FileSession { readonly file: string; @@ -32,8 +32,7 @@ export class SessionFiles implements BacktraceModule { private _cleared = false; constructor( - private readonly _fileSystem: FileSystem, - private readonly _directory: string, + private readonly _storage: BacktraceSyncStorage & BacktraceIterableSyncStorage, public readonly sessionId: SessionId, private readonly _maxPreviousLockedSessions = 1, private readonly _lockable = true, @@ -75,8 +74,7 @@ export class SessionFiles implements BacktraceModule { } return (this._previousSession = new SessionFiles( - this._fileSystem, - this._directory, + this._storage, lastSessionMarker.sessionId, this._maxPreviousLockedSessions - 1, this._maxPreviousLockedSessions > 0, @@ -128,11 +126,7 @@ export class SessionFiles implements BacktraceModule { public getFileName(prefix: string) { this.throwIfCleared(); - return ( - this._directory + - '/' + - `${this.escapeFileName(prefix)}_${this._escapedSessionId}_${this.sessionId.timestamp}` - ); + return `${this.escapeFileName(prefix)}_${this._escapedSessionId}_${this.sessionId.timestamp}`; } public getSessionFiles() { @@ -143,7 +137,7 @@ export class SessionFiles implements BacktraceModule { .map((file) => this.getFileSession(file)) .filter(isDefined) .filter(({ sessionId }) => this.sessionIdEquals(sessionId, this.sessionId)) - .map(({ file }) => this._directory + '/' + file); + .map(({ file }) => file); } public clearSession() { @@ -159,7 +153,7 @@ export class SessionFiles implements BacktraceModule { try { const sessionFiles = this.getSessionFiles(); for (const file of sessionFiles) { - this._fileSystem.unlinkSync(file); + this._storage.removeSync(file); } } catch { // Don't propagate errors @@ -204,14 +198,14 @@ export class SessionFiles implements BacktraceModule { private readDirectoryFiles() { try { - return this._fileSystem.readDirSync(this._directory); + return [...this._storage.keysSync()]; } catch { return []; } } private createSessionMarker() { - this._fileSystem.writeFileSync(this.marker, ''); + this._storage.setSync(this.marker, ''); } private escapeFileName(name: string) { diff --git a/packages/sdk-core/src/modules/storage/index.ts b/packages/sdk-core/src/modules/storage/index.ts index b8d48ac6..8beeb5ef 100644 --- a/packages/sdk-core/src/modules/storage/index.ts +++ b/packages/sdk-core/src/modules/storage/index.ts @@ -1,2 +1,2 @@ -export * from './FileSystem.js'; +export * from './BacktraceStorage.js'; export * from './SessionFiles.js'; diff --git a/packages/sdk-core/tests/_mocks/fileSystem.ts b/packages/sdk-core/tests/_mocks/fileSystem.ts deleted file mode 100644 index b82d01cd..00000000 --- a/packages/sdk-core/tests/_mocks/fileSystem.ts +++ /dev/null @@ -1,60 +0,0 @@ -import path from 'path'; -import { BacktraceFileAttachment } from '../../src/model/attachment/index.js'; -import { FileSystem } from '../../src/modules/storage/FileSystem.js'; -import { Mocked } from './types.js'; - -export type MockedFileSystem = Mocked & { files: Record }; - -export function mockFileSystem(files?: Record): MockedFileSystem { - const fs = Object.entries(files ?? {}) - .map(([k, v]) => [path.resolve(k), v]) - .reduce( - (obj, [k, v]) => { - obj[k] = v; - return obj; - }, - {} as Record, - ); - - function readDir(dir: string) { - return Object.keys(fs) - .filter((k) => path.dirname(k) === path.resolve(dir)) - .map((p) => path.basename(p)); - } - - return { - files: fs, - - readDir: jest.fn().mockImplementation((dir: string) => Promise.resolve(readDir(dir))), - readDirSync: jest.fn().mockImplementation(readDir), - - createDir: jest.fn().mockReturnValue(Promise.resolve()), - createDirSync: jest.fn(), - - readFile: jest.fn().mockImplementation((p: string) => Promise.resolve(fs[path.resolve(p)])), - readFileSync: jest.fn().mockImplementation((p: string) => fs[path.resolve(p)]), - - writeFile: jest.fn().mockImplementation((p: string, c: string) => Promise.resolve((fs[path.resolve(p)] = c))), - writeFileSync: jest.fn().mockImplementation((p: string, c: string) => (fs[path.resolve(p)] = c)), - - unlink: jest.fn().mockImplementation((p: string) => { - delete fs[path.resolve(p)]; - return Promise.resolve(); - }), - unlinkSync: jest.fn().mockImplementation((p: string) => { - delete fs[path.resolve(p)]; - }), - - exists: jest.fn().mockImplementation((p: string) => Promise.resolve(path.resolve(p) in fs)), - existsSync: jest.fn().mockImplementation((p: string) => path.resolve(p) in fs), - - createAttachment: jest.fn().mockImplementation( - (p: string) => - ({ - filePath: p, - name: path.basename(p), - get: jest.fn().mockReturnValue(fs[path.resolve(p)]), - }) as BacktraceFileAttachment, - ), - }; -} diff --git a/packages/sdk-core/tests/_mocks/storage.ts b/packages/sdk-core/tests/_mocks/storage.ts new file mode 100644 index 00000000..c8c61b90 --- /dev/null +++ b/packages/sdk-core/tests/_mocks/storage.ts @@ -0,0 +1,50 @@ +import { BacktraceStorageModule } from '../../src/index.js'; +import { Mocked } from './types.js'; + +export type MockedBacktraceStorage = Mocked & { files: Record }; + +export function mockBacktraceStorage(files?: Record): MockedBacktraceStorage { + const fs = Object.entries(files ?? {}).reduce( + (obj, [k, v]) => { + obj[k] = v; + return obj; + }, + {} as Record, + ); + + async function* keys() { + for (const key of Object.keys(fs)) { + yield key; + } + } + + function* keysSync() { + for (const key of Object.keys(fs)) { + yield key; + } + } + + return { + files: fs, + + keys: jest.fn().mockImplementation(keys), + keysSync: jest.fn().mockImplementation(keysSync), + + get: jest.fn().mockImplementation((p: string) => Promise.resolve(fs[p])), + getSync: jest.fn().mockImplementation((p: string) => fs[p]), + + set: jest.fn().mockImplementation((p: string, c: string) => Promise.resolve((fs[p] = c))), + setSync: jest.fn().mockImplementation((p: string, c: string) => (fs[p] = c)), + + remove: jest.fn().mockImplementation((p: string) => { + delete fs[p]; + return Promise.resolve(); + }), + removeSync: jest.fn().mockImplementation((p: string) => { + delete fs[p]; + }), + + has: jest.fn().mockImplementation((p: string) => Promise.resolve(p in fs)), + hasSync: jest.fn().mockImplementation((p: string) => p in fs), + }; +} diff --git a/packages/sdk-core/tests/database/databaseContextMemoryStorageTests.spec.ts b/packages/sdk-core/tests/database/databaseContextMemoryStorageTests.spec.ts index dc46264d..a40fda80 100644 --- a/packages/sdk-core/tests/database/databaseContextMemoryStorageTests.spec.ts +++ b/packages/sdk-core/tests/database/databaseContextMemoryStorageTests.spec.ts @@ -1,21 +1,19 @@ import assert from 'assert'; -import path from 'path'; -import { fileURLToPath } from 'url'; import { TimeHelper } from '../../src/common/TimeHelper.js'; -import { BacktraceData, BacktraceDatabaseRecord, BacktraceReportSubmissionResult } from '../../src/index.js'; +import { + BacktraceData, + BacktraceDatabaseConfiguration, + BacktraceReportSubmissionResult, + ReportBacktraceDatabaseRecord, +} from '../../src/index.js'; import { BacktraceDatabase } from '../../src/modules/database/BacktraceDatabase.js'; -import { mockFileSystem } from '../_mocks/fileSystem.js'; +import { mockBacktraceStorage } from '../_mocks/storage.js'; import { BacktraceTestClient } from '../mocks/BacktraceTestClient.js'; describe('Database context memory storage tests', () => { - const testDatabaseSettings = { + const testDatabaseSettings: BacktraceDatabaseConfiguration = { enable: true, autoSend: false, - // this option doesn't matter because we mock the database provider - // interface. However, if bug happen we want to be sure to not create - // anything. Instead we want to fail loud and hard. - createDatabaseDirectory: false, - path: path.join(path.dirname(fileURLToPath(import.meta.url)), 'database'), }; afterEach(() => { @@ -31,7 +29,7 @@ describe('Database context memory storage tests', () => { }, [], [], - mockFileSystem(), + mockBacktraceStorage(), ); const database = client.database as BacktraceDatabase; if (!database) { @@ -48,7 +46,9 @@ describe('Database context memory storage tests', () => { expect(records.length).toBe(1); assert(records[0].type === 'report'); - expect(records[0].data.attributes['error.message']).toEqual(testingErrorMessage); + expect((records[0] as ReportBacktraceDatabaseRecord).data.attributes['error.message']).toEqual( + testingErrorMessage, + ); }); it('Should remove report from the database after succesful submission', async () => { @@ -59,7 +59,7 @@ describe('Database context memory storage tests', () => { }, [], [], - mockFileSystem(), + mockBacktraceStorage(), ); const database = client.database as BacktraceDatabase; if (!database) { @@ -83,7 +83,7 @@ describe('Database context memory storage tests', () => { describe('Record load on the database start', () => { it('Shouldn not fail when no records are available in the database dir', () => { - const fileSystem = mockFileSystem(); + const fileSystem = mockBacktraceStorage(); const client = BacktraceTestClient.buildFakeClient( { @@ -98,17 +98,15 @@ describe('Database context memory storage tests', () => { }); it('Should load records from the storage provider to context', async () => { - const record: BacktraceDatabaseRecord = { + const record: ReportBacktraceDatabaseRecord = { type: 'report', - attachments: [], timestamp: TimeHelper.now(), data: {} as BacktraceData, id: '123', locked: false, - sessionId: undefined, }; - const fileSystem = mockFileSystem({ - [path.join(testDatabaseSettings.path, 'abc-record.json')]: JSON.stringify(record), + const fileSystem = mockBacktraceStorage({ + ['abc-record.json']: JSON.stringify(record), }); const client = BacktraceTestClient.buildFakeClient( { diff --git a/packages/sdk-core/tests/database/databaseContextValidationTests.spec.ts b/packages/sdk-core/tests/database/databaseContextValidationTests.spec.ts index 53c0951a..f82ac61c 100644 --- a/packages/sdk-core/tests/database/databaseContextValidationTests.spec.ts +++ b/packages/sdk-core/tests/database/databaseContextValidationTests.spec.ts @@ -1,9 +1,11 @@ import assert from 'assert'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { BacktraceDatabaseConfiguration, BacktraceReportSubmissionResult } from '../../src/index.js'; +import { + BacktraceDatabaseConfiguration, + BacktraceReportSubmissionResult, + ReportBacktraceDatabaseRecord, +} from '../../src/index.js'; import { BacktraceDatabase } from '../../src/modules/database/BacktraceDatabase.js'; -import { mockFileSystem } from '../_mocks/fileSystem.js'; +import { mockBacktraceStorage } from '../_mocks/storage.js'; import { BacktraceTestClient } from '../mocks/BacktraceTestClient.js'; describe('Database context validation tests', () => { @@ -11,11 +13,6 @@ describe('Database context validation tests', () => { const testDatabaseSettings: BacktraceDatabaseConfiguration = { enable: true, autoSend: false, - // this option doesn't matter because we mock the database provider - // interface. However, if bug happen we want to be sure to not create - // anything. Instead we want to fail loud and hard. - createDatabaseDirectory: false, - path: path.join(path.dirname(fileURLToPath(import.meta.url)), 'database'), }; afterEach(() => { @@ -40,7 +37,7 @@ describe('Database context validation tests', () => { }, [], [], - mockFileSystem(), + mockBacktraceStorage(), ); jest.spyOn(client.requestHandler, 'postError').mockResolvedValue( Promise.resolve(BacktraceReportSubmissionResult.OnInternalServerError('test')), @@ -59,7 +56,9 @@ describe('Database context validation tests', () => { const record = records[index]; const expectedMessage = overflowEvents + index; assert(record.type === 'report'); - expect(record.data.attributes['error.message']).toEqual(expectedMessage.toString()); + expect((record as ReportBacktraceDatabaseRecord).data.attributes['error.message']).toEqual( + expectedMessage.toString(), + ); } }); } diff --git a/packages/sdk-core/tests/database/databaseRecordBatchTests.spec.ts b/packages/sdk-core/tests/database/databaseRecordBatchTests.spec.ts index e5ace173..32a2e622 100644 --- a/packages/sdk-core/tests/database/databaseRecordBatchTests.spec.ts +++ b/packages/sdk-core/tests/database/databaseRecordBatchTests.spec.ts @@ -1,7 +1,5 @@ -import path from 'path'; -import { fileURLToPath } from 'url'; import { BacktraceReportSubmissionResult } from '../../src/index.js'; -import { mockFileSystem } from '../_mocks/fileSystem.js'; +import { mockBacktraceStorage } from '../_mocks/storage.js'; import { BacktraceTestClient } from '../mocks/BacktraceTestClient.js'; import { testHttpClient } from '../mocks/testHttpClient.js'; @@ -26,14 +24,13 @@ describe('Database record batch tests', () => { database: { enable: true, autoSend: true, - path: path.join(path.dirname(fileURLToPath(import.meta.url)), 'database'), maximumRetries, retryInterval: 1000, }, }, [], [], - mockFileSystem(), + mockBacktraceStorage(), ); const database = client.database; if (!database) { @@ -59,14 +56,13 @@ describe('Database record batch tests', () => { database: { enable: true, autoSend: true, - path: path.join(path.dirname(fileURLToPath(import.meta.url)), 'database'), maximumRetries, retryInterval: 1000, }, }, [], [], - mockFileSystem(), + mockBacktraceStorage(), ); const database = client.database; if (!database) { diff --git a/packages/sdk-core/tests/database/databaseSendTests.spec.ts b/packages/sdk-core/tests/database/databaseSendTests.spec.ts index d757c466..d812d093 100644 --- a/packages/sdk-core/tests/database/databaseSendTests.spec.ts +++ b/packages/sdk-core/tests/database/databaseSendTests.spec.ts @@ -1,8 +1,6 @@ -import path from 'path'; -import { fileURLToPath } from 'url'; import { BacktraceDatabaseConfiguration, BacktraceReportSubmissionResult } from '../../src/index.js'; import { BacktraceDatabase } from '../../src/modules/database/BacktraceDatabase.js'; -import { mockFileSystem } from '../_mocks/fileSystem.js'; +import { mockBacktraceStorage } from '../_mocks/storage.js'; import { BacktraceTestClient } from '../mocks/BacktraceTestClient.js'; describe('Database send tests', () => { @@ -12,11 +10,6 @@ describe('Database send tests', () => { const testDatabaseSettings: BacktraceDatabaseConfiguration = { enable: true, autoSend: false, - // this option doesn't matter because we mock the database provider - // interface. However, if bug happen we want to be sure to not create - // anything. Instead we want to fail loud and hard. - createDatabaseDirectory: false, - path: path.join(path.dirname(fileURLToPath(import.meta.url)), 'database'), }; describe('Flush', () => { @@ -27,7 +20,7 @@ describe('Database send tests', () => { }, [], [], - mockFileSystem(), + mockBacktraceStorage(), ); const database = client.database as BacktraceDatabase; if (!database) { @@ -52,7 +45,7 @@ describe('Database send tests', () => { }, [], [], - mockFileSystem(), + mockBacktraceStorage(), ); const database = client.database as BacktraceDatabase; if (!database) { @@ -83,7 +76,7 @@ describe('Database send tests', () => { }, [], [], - mockFileSystem(), + mockBacktraceStorage(), ); const database = client.database as BacktraceDatabase; if (!database) { @@ -109,7 +102,7 @@ describe('Database send tests', () => { }, [], [], - mockFileSystem(), + mockBacktraceStorage(), ); const database = client.database as BacktraceDatabase; if (!database) { diff --git a/packages/sdk-core/tests/database/databaseSetupTests.spec.ts b/packages/sdk-core/tests/database/databaseSetupTests.spec.ts index 4c7238da..0640756d 100644 --- a/packages/sdk-core/tests/database/databaseSetupTests.spec.ts +++ b/packages/sdk-core/tests/database/databaseSetupTests.spec.ts @@ -1,7 +1,15 @@ import crypto from 'crypto'; import { nextTick } from 'process'; import { promisify } from 'util'; -import { AttachmentBacktraceDatabaseRecord, BacktraceData, ReportBacktraceDatabaseRecord } from '../../src/index.js'; +import { + BacktraceData, + BacktraceDatabaseConfiguration, + BacktraceDatabaseRecord, + BacktraceDatabaseStorageProvider, + DefaultReportBacktraceDatabaseRecordFactory, + ReportBacktraceDatabaseRecord, + ReportBacktraceDatabaseRecordSender, +} from '../../src/index.js'; import { RequestBacktraceReportSubmission } from '../../src/model/http/BacktraceReportSubmission.js'; import { BacktraceDatabase } from '../../src/modules/database/BacktraceDatabase.js'; import { BacktraceDatabaseContext } from '../../src/modules/database/BacktraceDatabaseContext.js'; @@ -15,12 +23,11 @@ function randomReportRecord(): ReportBacktraceDatabaseRecord { type: 'report', data: { uuid: crypto.randomUUID() } as BacktraceData, locked: false, - attachments: [], timestamp: Date.now(), }; } -function randomAttachmentRecord(): AttachmentBacktraceDatabaseRecord { +function randomAttachmentRecord(): BacktraceDatabaseRecord { return { id: crypto.randomUUID(), type: 'attachment', @@ -29,39 +36,45 @@ function randomAttachmentRecord(): AttachmentBacktraceDatabaseRecord { attachment: { name: 'x', get: () => undefined }, rxid: 'x', sessionId: { id: crypto.randomUUID(), timestamp: Date.now() }, - }; + } as BacktraceDatabaseRecord; } describe('Database setup tests', () => { + function createDatabase( + options: BacktraceDatabaseConfiguration | undefined, + storageProvider: BacktraceDatabaseStorageProvider, + ) { + return new BacktraceDatabase( + options, + storageProvider, + { + report: new ReportBacktraceDatabaseRecordSender( + new RequestBacktraceReportSubmission( + { + url: TEST_SUBMISSION_URL, + }, + testHttpClient, + ), + ), + }, + DefaultReportBacktraceDatabaseRecordFactory.default(), + ); + } + it('The database should be disabled by default', () => { const testStorageProvider = getTestStorageProvider(); - const database = new BacktraceDatabase( - undefined, - testStorageProvider, - new RequestBacktraceReportSubmission( - { - url: TEST_SUBMISSION_URL, - }, - testHttpClient, - ), - ); + const database = createDatabase(undefined, testStorageProvider); expect(database.enabled).toBeFalsy(); }); it('Should be enabled after returning true from the start method', () => { const testStorageProvider = getTestStorageProvider(); - const database = new BacktraceDatabase( + const database = createDatabase( { autoSend: false, }, testStorageProvider, - new RequestBacktraceReportSubmission( - { - url: TEST_SUBMISSION_URL, - }, - testHttpClient, - ), ); const databaseStartResult = database.initialize(); @@ -72,16 +85,7 @@ describe('Database setup tests', () => { it('Should not enable the database if the enable option is set to false', () => { const testStorageProvider = getTestStorageProvider(); - const database = new BacktraceDatabase( - { enable: false }, - testStorageProvider, - new RequestBacktraceReportSubmission( - { - url: TEST_SUBMISSION_URL, - }, - testHttpClient, - ), - ); + const database = createDatabase({ enable: false }, testStorageProvider); const databaseStartResult = database.initialize(); @@ -91,18 +95,11 @@ describe('Database setup tests', () => { it('Should not enable the database if the storage is not prepared', () => { const testStorageProvider = getTestStorageProvider(); - const database = new BacktraceDatabase( + const database = createDatabase( { enable: true, - path: '/path/to/fake/dir', }, testStorageProvider, - new RequestBacktraceReportSubmission( - { - url: TEST_SUBMISSION_URL, - }, - testHttpClient, - ), ); jest.spyOn(testStorageProvider, 'start').mockReturnValue(false); @@ -114,16 +111,7 @@ describe('Database setup tests', () => { it('Should be disabled after disposing database', () => { const testStorageProvider = getTestStorageProvider(); - const database = new BacktraceDatabase( - undefined, - testStorageProvider, - new RequestBacktraceReportSubmission( - { - url: TEST_SUBMISSION_URL, - }, - testHttpClient, - ), - ); + const database = createDatabase(undefined, testStorageProvider); database.initialize(); database.dispose(); @@ -133,16 +121,7 @@ describe('Database setup tests', () => { it('Should not add a record to disabled database', () => { const testStorageProvider = getTestStorageProvider(); - const database = new BacktraceDatabase( - undefined, - testStorageProvider, - new RequestBacktraceReportSubmission( - { - url: TEST_SUBMISSION_URL, - }, - testHttpClient, - ), - ); + const database = createDatabase(undefined, testStorageProvider); const result = database.add({} as BacktraceData, []); expect(result).toBeFalsy(); @@ -155,17 +134,11 @@ describe('Database setup tests', () => { const contextLoad = jest.spyOn(BacktraceDatabaseContext.prototype, 'load'); - const database = new BacktraceDatabase( + const database = createDatabase( { autoSend: false, }, testStorageProvider, - new RequestBacktraceReportSubmission( - { - url: TEST_SUBMISSION_URL, - }, - testHttpClient, - ), ); const databaseStartResult = database.initialize(); @@ -187,18 +160,12 @@ describe('Database setup tests', () => { const contextLoad = jest.spyOn(BacktraceDatabaseContext.prototype, 'load'); - const database = new BacktraceDatabase( + const database = createDatabase( { autoSend: false, maximumNumberOfRecords: 2, }, testStorageProvider, - new RequestBacktraceReportSubmission( - { - url: TEST_SUBMISSION_URL, - }, - testHttpClient, - ), ); const databaseStartResult = database.initialize(); @@ -237,16 +204,24 @@ describe('Database setup tests', () => { const database = new BacktraceDatabase( { autoSend: false, - maximumNumberOfRecords: 2, - maximumNumberOfAttachmentRecords: 3, }, testStorageProvider, - new RequestBacktraceReportSubmission( - { - url: TEST_SUBMISSION_URL, - }, - testHttpClient, - ), + { + report: new ReportBacktraceDatabaseRecordSender( + new RequestBacktraceReportSubmission( + { + url: TEST_SUBMISSION_URL, + }, + testHttpClient, + ), + ), + }, + DefaultReportBacktraceDatabaseRecordFactory.default(), + undefined, + { + report: 2, + attachment: 3, + }, ); const databaseStartResult = database.initialize(); diff --git a/packages/sdk-core/tests/database/databaseStorageFlowTests.spec.ts b/packages/sdk-core/tests/database/databaseStorageFlowTests.spec.ts index e3b9ef9c..8a664fa0 100644 --- a/packages/sdk-core/tests/database/databaseStorageFlowTests.spec.ts +++ b/packages/sdk-core/tests/database/databaseStorageFlowTests.spec.ts @@ -2,7 +2,7 @@ import path from 'path'; import { fileURLToPath } from 'url'; import { BacktraceReportSubmissionResult } from '../../src/index.js'; import { BacktraceDatabase } from '../../src/modules/database/BacktraceDatabase.js'; -import { mockFileSystem } from '../_mocks/fileSystem.js'; +import { mockBacktraceStorage } from '../_mocks/storage.js'; import { BacktraceTestClient } from '../mocks/BacktraceTestClient.js'; describe('Database storage provider flow tests', () => { const testDatabaseSettings = { @@ -28,7 +28,7 @@ describe('Database storage provider flow tests', () => { }, [], [], - mockFileSystem(), + mockBacktraceStorage(), ); const database = client.database as BacktraceDatabase; if (!database) { @@ -54,14 +54,13 @@ describe('Database storage provider flow tests', () => { }, [], [], - mockFileSystem(), + mockBacktraceStorage(), ); const database = client.database as BacktraceDatabase; if (!database) { throw new Error('Invalid database setup. Database must be defined!'); } - const addSpy = jest.spyOn(database, 'add'); const removeSpy = jest.spyOn(database, 'remove'); jest.spyOn(client.requestHandler, 'postError').mockResolvedValue( @@ -70,7 +69,6 @@ describe('Database storage provider flow tests', () => { await client.send(new Error(testingErrorMessage)); - expect(addSpy).toHaveBeenCalled(); expect(removeSpy).toHaveBeenCalled(); }); }); diff --git a/packages/sdk-core/tests/mocks/BacktraceTestClient.ts b/packages/sdk-core/tests/mocks/BacktraceTestClient.ts index 57ba8c01..a0da7073 100644 --- a/packages/sdk-core/tests/mocks/BacktraceTestClient.ts +++ b/packages/sdk-core/tests/mocks/BacktraceTestClient.ts @@ -5,7 +5,7 @@ import { BacktraceCoreClient, BacktraceDatabaseStorageProvider, BacktraceRequestHandler, - FileSystem, + BacktraceStorageModule, } from '../../src/index.js'; import { testHttpClient } from '../mocks/testHttpClient.js'; export const TOKEN = '590d39eb154cff1d30f2b689f9a928bb592b25e7e7c10192fe208485ea68d91c'; @@ -21,7 +21,7 @@ export class BacktraceTestClient extends BacktraceCoreClient { handler: BacktraceRequestHandler, attributeProviders: BacktraceAttributeProvider[] = [], attachments: BacktraceAttachment[] = [], - fileSystem?: FileSystem, + storage?: BacktraceStorageModule, ) { super({ options: { @@ -41,7 +41,9 @@ export class BacktraceTestClient extends BacktraceCoreClient { }, requestHandler: handler, attributeProviders, - fileSystem, + database: { + storage, + }, }); this.requestHandler = handler; } @@ -50,7 +52,7 @@ export class BacktraceTestClient extends BacktraceCoreClient { options: Partial = {}, attributeProviders: BacktraceAttributeProvider[] = [], attachments: BacktraceAttachment[] = [], - fileSystem?: FileSystem, + storage?: BacktraceStorageModule, ) { attributeProviders.push({ type: 'scoped', @@ -61,7 +63,7 @@ export class BacktraceTestClient extends BacktraceCoreClient { }; }, }); - const instance = new BacktraceTestClient(options, testHttpClient, attributeProviders, attachments, fileSystem); + const instance = new BacktraceTestClient(options, testHttpClient, attributeProviders, attachments, storage); instance.initialize(); return instance; } diff --git a/packages/sdk-core/tests/storage/SessionFiles.spec.ts b/packages/sdk-core/tests/storage/SessionFiles.spec.ts index a457f535..51f9d30a 100644 --- a/packages/sdk-core/tests/storage/SessionFiles.spec.ts +++ b/packages/sdk-core/tests/storage/SessionFiles.spec.ts @@ -1,6 +1,6 @@ import assert from 'assert'; import { SessionFiles, SessionId } from '../../src/modules/storage/SessionFiles.js'; -import { mockFileSystem } from '../_mocks/fileSystem.js'; +import { mockBacktraceStorage } from '../_mocks/storage.js'; describe('SessionFiles', () => { function sessionId(id: string, timestamp?: number): SessionId { @@ -11,17 +11,17 @@ describe('SessionFiles', () => { } it('should create empty session marker on initialize', () => { - const fileSystem = mockFileSystem(); - const session = new SessionFiles(fileSystem, '.', sessionId('sessionId')); + const storage = mockBacktraceStorage(); + const session = new SessionFiles(storage, sessionId('sessionId')); session.initialize(); - expect(fileSystem.readFileSync(session.marker)).toEqual(''); + expect(storage.getSync(session.marker)).toEqual(''); }); describe('getPreviousSession', () => { it('should return undefined if no previous session marker exists', () => { - const fileSystem = mockFileSystem(); - const session = new SessionFiles(fileSystem, '.', sessionId('sessionId')); + const storage = mockBacktraceStorage(); + const session = new SessionFiles(storage, sessionId('sessionId')); session.initialize(); const previous = session.getPreviousSession(); @@ -29,11 +29,11 @@ describe('SessionFiles', () => { }); it('should return previous session if previous session marker exists', () => { - const fileSystem = mockFileSystem(); - const previousSession = new SessionFiles(fileSystem, '.', sessionId('previousSessionId', 10)); + const storage = mockBacktraceStorage(); + const previousSession = new SessionFiles(storage, sessionId('previousSessionId', 10)); previousSession.initialize(); - const session = new SessionFiles(fileSystem, '.', sessionId('sessionId', 20)); + const session = new SessionFiles(storage, sessionId('sessionId', 20)); session.initialize(); const previous = session.getPreviousSession(); @@ -41,11 +41,11 @@ describe('SessionFiles', () => { }); it('should return previous session if previous session contains underscores', () => { - const fileSystem = mockFileSystem(); - const previousSession = new SessionFiles(fileSystem, '.', sessionId('previous_session_id', 10)); + const storage = mockBacktraceStorage(); + const previousSession = new SessionFiles(storage, sessionId('previous_session_id', 10)); previousSession.initialize(); - const session = new SessionFiles(fileSystem, '.', sessionId('sessionId', 20)); + const session = new SessionFiles(storage, sessionId('sessionId', 20)); session.initialize(); const previous = session.getPreviousSession(); @@ -53,15 +53,15 @@ describe('SessionFiles', () => { }); it('should return session older than current if multiple previous session markers exist', () => { - const fileSystem = mockFileSystem(); - const oldPreviousSession = new SessionFiles(fileSystem, '.', sessionId('oldPreviousSessionId', 5)); - const previousSession = new SessionFiles(fileSystem, '.', sessionId('previousSessionId', 10)); - const nextSession = new SessionFiles(fileSystem, '.', sessionId('nextSessionId', 25)); + const storage = mockBacktraceStorage(); + const oldPreviousSession = new SessionFiles(storage, sessionId('oldPreviousSessionId', 5)); + const previousSession = new SessionFiles(storage, sessionId('previousSessionId', 10)); + const nextSession = new SessionFiles(storage, sessionId('nextSessionId', 25)); oldPreviousSession.initialize(); previousSession.initialize(); nextSession.initialize(); - const session = new SessionFiles(fileSystem, '.', sessionId('sessionId', 20)); + const session = new SessionFiles(storage, sessionId('sessionId', 20)); session.initialize(); const previous = session.getPreviousSession(); @@ -69,15 +69,15 @@ describe('SessionFiles', () => { }); it('should return previous session of each session', () => { - const fileSystem = mockFileSystem(); - const session1 = new SessionFiles(fileSystem, '.', sessionId('session1', 5)); - const session2 = new SessionFiles(fileSystem, '.', sessionId('session2', 10)); - const session3 = new SessionFiles(fileSystem, '.', sessionId('session3', 15)); + const storage = mockBacktraceStorage(); + const session1 = new SessionFiles(storage, sessionId('session1', 5)); + const session2 = new SessionFiles(storage, sessionId('session2', 10)); + const session3 = new SessionFiles(storage, sessionId('session3', 15)); session1.initialize(); session2.initialize(); session3.initialize(); - const session = new SessionFiles(fileSystem, '.', sessionId('sessionId', 20)); + const session = new SessionFiles(storage, sessionId('sessionId', 20)); session.initialize(); const actualSession3 = session.getPreviousSession(); @@ -92,15 +92,15 @@ describe('SessionFiles', () => { }); it('should return non-lockable session when maxPreviousLockedSessions is 0', () => { - const fileSystem = mockFileSystem(); - const session1 = new SessionFiles(fileSystem, '.', sessionId('session1', 5)); - const session2 = new SessionFiles(fileSystem, '.', sessionId('session2', 10)); - const session3 = new SessionFiles(fileSystem, '.', sessionId('session3', 15)); + const storage = mockBacktraceStorage(); + const session1 = new SessionFiles(storage, sessionId('session1', 5)); + const session2 = new SessionFiles(storage, sessionId('session2', 10)); + const session3 = new SessionFiles(storage, sessionId('session3', 15)); session1.initialize(); session2.initialize(); session3.initialize(); - const session = new SessionFiles(fileSystem, '.', sessionId('sessionId', 20), 0); + const session = new SessionFiles(storage, sessionId('sessionId', 20), 0); session.initialize(); const previousSession = session.getPreviousSession(); @@ -110,15 +110,15 @@ describe('SessionFiles', () => { }); it('should return lockable session when maxPreviousLockedSessions is larger than 0', () => { - const fileSystem = mockFileSystem(); - const session1 = new SessionFiles(fileSystem, '.', sessionId('session1', 5)); - const session2 = new SessionFiles(fileSystem, '.', sessionId('session2', 10)); - const session3 = new SessionFiles(fileSystem, '.', sessionId('session3', 15)); + const storage = mockBacktraceStorage(); + const session1 = new SessionFiles(storage, sessionId('session1', 5)); + const session2 = new SessionFiles(storage, sessionId('session2', 10)); + const session3 = new SessionFiles(storage, sessionId('session3', 15)); session1.initialize(); session2.initialize(); session3.initialize(); - const session = new SessionFiles(fileSystem, '.', sessionId('sessionId', 20), 1); + const session = new SessionFiles(storage, sessionId('sessionId', 20), 1); session.initialize(); const previousSession = session.getPreviousSession(); @@ -128,15 +128,15 @@ describe('SessionFiles', () => { }); it('should return non-lockable session from previous session when maxPreviousLockedSessions is 1', () => { - const fileSystem = mockFileSystem(); - const session1 = new SessionFiles(fileSystem, '.', sessionId('session1', 5)); - const session2 = new SessionFiles(fileSystem, '.', sessionId('session2', 10)); - const session3 = new SessionFiles(fileSystem, '.', sessionId('session3', 15)); + const storage = mockBacktraceStorage(); + const session1 = new SessionFiles(storage, sessionId('session1', 5)); + const session2 = new SessionFiles(storage, sessionId('session2', 10)); + const session3 = new SessionFiles(storage, sessionId('session3', 15)); session1.initialize(); session2.initialize(); session3.initialize(); - const session = new SessionFiles(fileSystem, '.', sessionId('sessionId', 20), 0); + const session = new SessionFiles(storage, sessionId('sessionId', 20), 0); session.initialize(); const previousSession = session.getPreviousSession()?.getPreviousSession(); @@ -148,15 +148,15 @@ describe('SessionFiles', () => { describe('getPreviousSessions', () => { it('should return all previous sessions', () => { - const fileSystem = mockFileSystem(); - const session1 = new SessionFiles(fileSystem, '.', sessionId('session1', 5)); - const session2 = new SessionFiles(fileSystem, '.', sessionId('session2', 10)); - const session3 = new SessionFiles(fileSystem, '.', sessionId('session3', 15)); + const storage = mockBacktraceStorage(); + const session1 = new SessionFiles(storage, sessionId('session1', 5)); + const session2 = new SessionFiles(storage, sessionId('session2', 10)); + const session3 = new SessionFiles(storage, sessionId('session3', 15)); session1.initialize(); session2.initialize(); session3.initialize(); - const session = new SessionFiles(fileSystem, '.', sessionId('sessionId', 20)); + const session = new SessionFiles(storage, sessionId('sessionId', 20)); session.initialize(); const previousSessions = session.getPreviousSessions(); @@ -169,15 +169,15 @@ describe('SessionFiles', () => { }); it('should return previous sessions limited by count', () => { - const fileSystem = mockFileSystem(); - const session1 = new SessionFiles(fileSystem, '.', sessionId('session1', 5)); - const session2 = new SessionFiles(fileSystem, '.', sessionId('session2', 10)); - const session3 = new SessionFiles(fileSystem, '.', sessionId('session3', 15)); + const storage = mockBacktraceStorage(); + const session1 = new SessionFiles(storage, sessionId('session1', 5)); + const session2 = new SessionFiles(storage, sessionId('session2', 10)); + const session3 = new SessionFiles(storage, sessionId('session3', 15)); session1.initialize(); session2.initialize(); session3.initialize(); - const session = new SessionFiles(fileSystem, '.', sessionId('sessionId', 20)); + const session = new SessionFiles(storage, sessionId('sessionId', 20)); session.initialize(); const previousSessions = session.getPreviousSessions(2); @@ -188,33 +188,33 @@ describe('SessionFiles', () => { describe('getFileName', () => { it('should return file name with escaped session ID', () => { - const fileSystem = mockFileSystem(); - const session = new SessionFiles(fileSystem, '.', sessionId('sessionId')); + const storage = mockBacktraceStorage(); + const session = new SessionFiles(storage, sessionId('sessionId')); const filename = session.getFileName('file_name'); expect(filename).toContain('file__name'); }); it('should return file name with escaped file name', () => { - const fileSystem = mockFileSystem(); - const session = new SessionFiles(fileSystem, '.', sessionId('session_id')); + const storage = mockBacktraceStorage(); + const session = new SessionFiles(storage, sessionId('session_id')); const filename = session.getFileName('file_name'); expect(filename).toContain('session__id'); }); it('should return file name with session timestamp', () => { - const fileSystem = mockFileSystem(); + const storage = mockBacktraceStorage(); const timestamp = 123812412; - const session = new SessionFiles(fileSystem, '.', sessionId('session_id', timestamp)); + const session = new SessionFiles(storage, sessionId('session_id', timestamp)); const filename = session.getFileName('file_name'); expect(filename).toContain(timestamp.toString()); }); it('should throw after clearing', () => { - const fileSystem = mockFileSystem(); - const session = new SessionFiles(fileSystem, '.', sessionId('session_id')); + const storage = mockBacktraceStorage(); + const session = new SessionFiles(storage, sessionId('session_id')); session.initialize(); session.clearSession(); @@ -225,14 +225,14 @@ describe('SessionFiles', () => { describe('getSessionFiles', () => { it('should return files matching session', () => { - const fileSystem = mockFileSystem(); + const storage = mockBacktraceStorage(); - const session = new SessionFiles(fileSystem, '.', sessionId('sessionId')); + const session = new SessionFiles(storage, sessionId('sessionId')); session.initialize(); const files = [session.getFileName('file1'), session.getFileName('file2'), session.getFileName('file3')]; for (const file of files) { - fileSystem.writeFileSync(file, ''); + storage.setSync(file, ''); } const actual = session.getSessionFiles(); @@ -240,18 +240,18 @@ describe('SessionFiles', () => { }); it('should not return files not matching session', () => { - const fileSystem = mockFileSystem(); + const storage = mockBacktraceStorage(); - const session = new SessionFiles(fileSystem, '.', sessionId('sessionId')); + const session = new SessionFiles(storage, sessionId('sessionId')); session.initialize(); - const session1 = new SessionFiles(fileSystem, '.', sessionId('session1')); - const session2 = new SessionFiles(fileSystem, '.', sessionId('session2')); - const session3 = new SessionFiles(fileSystem, '.', sessionId('session3')); + const session1 = new SessionFiles(storage, sessionId('session1')); + const session2 = new SessionFiles(storage, sessionId('session2')); + const session3 = new SessionFiles(storage, sessionId('session3')); const files = [session1.getFileName('file1'), session2.getFileName('file2'), session3.getFileName('file3')]; for (const file of files) { - fileSystem.writeFileSync(file, ''); + storage.setSync(file, ''); } const actual = session.getSessionFiles(); @@ -259,8 +259,8 @@ describe('SessionFiles', () => { }); it('should throw after clearing', () => { - const fileSystem = mockFileSystem(); - const session = new SessionFiles(fileSystem, '.', sessionId('session_id')); + const storage = mockBacktraceStorage(); + const session = new SessionFiles(storage, sessionId('session_id')); session.initialize(); session.clearSession(); @@ -271,59 +271,59 @@ describe('SessionFiles', () => { describe('clearSession', () => { it('should remove all files matching session', () => { - const fileSystem = mockFileSystem(); - const session = new SessionFiles(fileSystem, '.', sessionId('sessionId')); + const storage = mockBacktraceStorage(); + const session = new SessionFiles(storage, sessionId('sessionId')); session.initialize(); const files = [session.getFileName('file1'), session.getFileName('file2'), session.getFileName('file3')]; for (const file of files) { - fileSystem.writeFileSync(file, 'abc'); + storage.setSync(file, 'abc'); } session.clearSession(); for (const file of files) { - expect(fileSystem.files[file]).toBeUndefined(); + expect(storage.files[file]).toBeUndefined(); } }); it('should not remove files not matching session', () => { - const fileSystem = mockFileSystem(); - const session = new SessionFiles(fileSystem, '.', sessionId('sessionId')); + const storage = mockBacktraceStorage(); + const session = new SessionFiles(storage, sessionId('sessionId')); session.initialize(); - const session1 = new SessionFiles(fileSystem, '.', sessionId('session1')); - const session2 = new SessionFiles(fileSystem, '.', sessionId('session2')); - const session3 = new SessionFiles(fileSystem, '.', sessionId('session3')); + const session1 = new SessionFiles(storage, sessionId('session1')); + const session2 = new SessionFiles(storage, sessionId('session2')); + const session3 = new SessionFiles(storage, sessionId('session3')); const files = [session1.getFileName('file1'), session2.getFileName('file2'), session3.getFileName('file3')]; for (const file of files) { - fileSystem.writeFileSync(file, 'abc'); + storage.setSync(file, 'abc'); } session.clearSession(); for (const file of files) { - expect(fileSystem.readFileSync(file)).toEqual('abc'); + expect(storage.getSync(file)).toEqual('abc'); } }); it('should remove session marker', () => { - const fileSystem = mockFileSystem(); - const session = new SessionFiles(fileSystem, '.', sessionId('sessionId')); + const storage = mockBacktraceStorage(); + const session = new SessionFiles(storage, sessionId('sessionId')); session.initialize(); session.clearSession(); - expect(fileSystem.readFileSync(session.marker)).toBeUndefined(); + expect(storage.getSync(session.marker)).toBeUndefined(); }); }); describe('locking and unlocking', () => { describe('lock', () => { it('should return non-empty lock id when lock id is not provided', () => { - const fileSystem = mockFileSystem(); - const session = new SessionFiles(fileSystem, '.', sessionId('sessionId')); + const storage = mockBacktraceStorage(); + const session = new SessionFiles(storage, sessionId('sessionId')); session.initialize(); const lockId = session.lock(); @@ -332,8 +332,8 @@ describe('SessionFiles', () => { }); it('should return provided lock id when lock id is provided', () => { - const fileSystem = mockFileSystem(); - const session = new SessionFiles(fileSystem, '.', sessionId('sessionId')); + const storage = mockBacktraceStorage(); + const session = new SessionFiles(storage, sessionId('sessionId')); session.initialize(); const lockId = session.lock('lockId'); @@ -342,8 +342,8 @@ describe('SessionFiles', () => { }); it('should return undefined after cleared', () => { - const fileSystem = mockFileSystem(); - const session = new SessionFiles(fileSystem, '.', sessionId('sessionId')); + const storage = mockBacktraceStorage(); + const session = new SessionFiles(storage, sessionId('sessionId')); session.initialize(); session.clearSession(); @@ -353,8 +353,8 @@ describe('SessionFiles', () => { }); it('should return undefined when not lockable', () => { - const fileSystem = mockFileSystem(); - const session = new SessionFiles(fileSystem, '.', sessionId('sessionId'), undefined, false); + const storage = mockBacktraceStorage(); + const session = new SessionFiles(storage, sessionId('sessionId'), undefined, false); session.initialize(); session.clearSession(); @@ -366,25 +366,25 @@ describe('SessionFiles', () => { describe('clearSession', () => { it('should not clear files when session is locked', () => { - const fileSystem = mockFileSystem(); - const session = new SessionFiles(fileSystem, '.', sessionId('sessionId')); + const storage = mockBacktraceStorage(); + const session = new SessionFiles(storage, sessionId('sessionId')); session.initialize(); - const expected = { ...fileSystem.files }; + const expected = { ...storage.files }; session.lock(); session.clearSession(); - expect(fileSystem.files).toEqual(expected); + expect(storage.files).toEqual(expected); }); it('should clear files after unlocking when clearSession is called before', () => { - const fileSystem = mockFileSystem(); - const session = new SessionFiles(fileSystem, '.', sessionId('sessionId')); + const storage = mockBacktraceStorage(); + const session = new SessionFiles(storage, sessionId('sessionId')); session.initialize(); - const expected = { ...fileSystem.files }; + const expected = { ...storage.files }; const lockId = session.lock(); assert(lockId); @@ -392,29 +392,29 @@ describe('SessionFiles', () => { session.clearSession(); session.unlock(lockId); - expect(fileSystem.files).not.toEqual(expected); + expect(storage.files).not.toEqual(expected); }); it('should not clear files after unlocking when clearSession is not called before', () => { - const fileSystem = mockFileSystem(); - const session = new SessionFiles(fileSystem, '.', sessionId('sessionId')); + const storage = mockBacktraceStorage(); + const session = new SessionFiles(storage, sessionId('sessionId')); session.initialize(); - const expected = { ...fileSystem.files }; + const expected = { ...storage.files }; const lockId = session.lock(); assert(lockId); session.unlock(lockId); - expect(fileSystem.files).toEqual(expected); + expect(storage.files).toEqual(expected); }); it('should not clear files after unlocking when locked with other locks', () => { - const fileSystem = mockFileSystem(); - const session = new SessionFiles(fileSystem, '.', sessionId('sessionId')); + const storage = mockBacktraceStorage(); + const session = new SessionFiles(storage, sessionId('sessionId')); session.initialize(); - const expected = { ...fileSystem.files }; + const expected = { ...storage.files }; session.lock(); const lockId = session.lock(); @@ -423,7 +423,7 @@ describe('SessionFiles', () => { session.clearSession(); session.unlock(lockId); - expect(fileSystem.files).toEqual(expected); + expect(storage.files).toEqual(expected); }); }); });