{"version":3,"file":"screenshot-item.mjs","sources":["../../src/screenshot-item.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport { uuid } from '@midscene/shared/utils';\nimport { extractImageByIdSync } from './dump/html-utils';\nimport {\n  type ScreenshotRef,\n  normalizeScreenshotRef,\n} from './dump/screenshot-store';\n\n/**\n * Serialization format for ScreenshotItem\n * - { $screenshot: \"id\" } - inline mode, references imageMap in HTML\n * - { base64: \"path\" } - directory mode, references external file path\n */\nexport type ScreenshotSerializeFormat = ScreenshotRef;\n\n/**\n * Detect image format from base64 data URI prefix.\n */\nfunction detectFormat(base64: string): 'png' | 'jpeg' {\n  if (base64.startsWith('data:image/jpeg')) return 'jpeg';\n  if (base64.startsWith('data:image/jpg')) return 'jpeg';\n  return 'png';\n}\n\n/**\n * ScreenshotItem encapsulates screenshot data.\n *\n * Supports lazy loading after memory release:\n * - inline mode: reads from HTML file using streaming (extractImageByIdSync)\n * - directory mode: reads from file on disk\n *\n * After persistence, memory is released but the screenshot can be recovered\n * on-demand from disk, making it safe to release memory at any time.\n */\nexport class ScreenshotItem {\n  private _id: string;\n  private _base64: string | null;\n  private _format: 'png' | 'jpeg';\n  private _capturedAt: number;\n  private _serializedRef: ScreenshotRef | null = null;\n  private _persistedPath: string | null = null;\n  private _persistedHtmlPath: string | null = null;\n\n  private constructor(id: string, base64: string, capturedAt: number) {\n    this._id = id;\n    this._base64 = base64;\n    this._format = detectFormat(base64);\n    this._capturedAt = capturedAt;\n  }\n\n  /** Create a new ScreenshotItem from base64 data */\n  static create(base64: string, capturedAt: number): ScreenshotItem {\n    return new ScreenshotItem(uuid(), base64, capturedAt);\n  }\n\n  get id(): string {\n    return this._id;\n  }\n\n  /** Get the image format (png or jpeg) */\n  get format(): 'png' | 'jpeg' {\n    return this._format;\n  }\n\n  /** Get the file extension for this screenshot */\n  get extension(): string {\n    return this._format === 'jpeg' ? 'jpeg' : 'png';\n  }\n\n  /** Get screenshot capture timestamp in milliseconds */\n  get capturedAt(): number {\n    return this._capturedAt;\n  }\n\n  get base64(): string {\n    // If data is in memory, return it directly\n    if (this._base64 !== null) {\n      return this._base64;\n    }\n\n    const loadFromFile = (): string => {\n      if (this._persistedPath === null) {\n        throw new Error(`Screenshot ${this._id}: file recovery path missing`);\n      }\n      const buffer = readFileSync(this._persistedPath);\n      return `data:image/${this._format};base64,${buffer.toString('base64')}`;\n    };\n\n    const loadFromInline = (): string => {\n      if (this._persistedHtmlPath === null) {\n        throw new Error(`Screenshot ${this._id}: HTML recovery path missing`);\n      }\n      const data = extractImageByIdSync(this._persistedHtmlPath, this._id);\n      if (data) {\n        return data;\n      }\n      throw new Error(\n        `Screenshot ${this._id}: cannot recover from HTML (id not found in ${this._persistedHtmlPath})`,\n      );\n    };\n\n    // Recover from the primary serialized mode first.\n    if (this._serializedRef?.storage === 'file') {\n      return loadFromFile();\n    }\n\n    if (this._serializedRef?.storage === 'inline') {\n      return loadFromInline();\n    }\n\n    // Fall back to whichever recovery path is available.\n    if (this._persistedPath !== null) {\n      return loadFromFile();\n    }\n\n    if (this._persistedHtmlPath !== null) {\n      return loadFromInline();\n    }\n\n    throw new Error(\n      `Screenshot ${this._id}: base64 data released without recovery path`,\n    );\n  }\n\n  /** Check if base64 data is still available in memory (not yet released) */\n  hasBase64(): boolean {\n    return this._base64 !== null;\n  }\n\n  /**\n   * Mark as persisted to HTML (inline mode).\n   * Releases base64 memory, but keeps HTML path for lazy loading recovery.\n   * @param htmlPath - absolute path to the HTML file containing the image\n   */\n  markPersistedInline(htmlPath: string): ScreenshotRef {\n    const ref = this.createRef('inline');\n    this._serializedRef = ref;\n    this._persistedHtmlPath = htmlPath;\n    this._base64 = null;\n    return ref;\n  }\n\n  /**\n   * Register a file-backed recovery path without changing the serialized mode.\n   * Used when inline persistence also needs a shared file copy next to dumps.\n   */\n  registerPersistedFileCopy(\n    relativePath: string,\n    absolutePath: string,\n  ): ScreenshotRef {\n    const ref = this.createRef('file', relativePath);\n    this._persistedPath = absolutePath;\n    this._base64 = null;\n    return ref;\n  }\n\n  /**\n   * Mark as persisted to file (directory mode).\n   * Releases base64 memory, but keeps file path for lazy loading recovery.\n   * @param relativePath - relative path for serialization (e.g., \"./screenshots/id.jpeg\")\n   * @param absolutePath - absolute path for lazy loading recovery\n   */\n  markPersistedToPath(\n    relativePath: string,\n    absolutePath: string,\n  ): ScreenshotRef {\n    const ref = this.registerPersistedFileCopy(relativePath, absolutePath);\n    this._serializedRef = ref;\n    return ref;\n  }\n\n  /** Serialize for JSON - format depends on persistence state */\n  toSerializable(): ScreenshotSerializeFormat {\n    return (\n      this._serializedRef ?? {\n        type: 'midscene_screenshot_ref',\n        id: this._id,\n        capturedAt: this._capturedAt,\n        mimeType: this._format === 'jpeg' ? 'image/jpeg' : 'image/png',\n        storage: 'inline',\n      }\n    );\n  }\n\n  /** Check if a value is a serialized ScreenshotItem reference (inline or directory mode) */\n  static isSerialized(value: unknown): value is ScreenshotSerializeFormat {\n    return normalizeScreenshotRef(value) !== null;\n  }\n\n  private createRef(\n    storage: 'inline' | 'file',\n    relativePath?: string,\n  ): ScreenshotRef {\n    const baseRef: Omit<ScreenshotRef, 'path'> = {\n      type: 'midscene_screenshot_ref',\n      id: this._id,\n      capturedAt: this._capturedAt,\n      mimeType: this._format === 'jpeg' ? 'image/jpeg' : 'image/png',\n      storage,\n    };\n    if (storage === 'file') {\n      return {\n        ...baseRef,\n        storage,\n        path: relativePath!,\n      };\n    }\n    return baseRef;\n  }\n\n  /**\n   * Get base64 data without the data URI prefix.\n   * Useful for writing raw binary data to files.\n   */\n  get rawBase64(): string {\n    return this.base64.replace(/^data:image\\/(png|jpeg|jpg);base64,/, '');\n  }\n}\n"],"names":["detectFormat","base64","ScreenshotItem","capturedAt","uuid","loadFromFile","Error","buffer","readFileSync","loadFromInline","data","extractImageByIdSync","htmlPath","ref","relativePath","absolutePath","value","normalizeScreenshotRef","storage","baseRef","id"],"mappings":"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"}