editSession.ts 2.72 KB
// 结构化编辑会话:每个模型层持有一份 yaml Document(保留注释),编辑走「按路径改节点」,
// 只动被编辑的节点、其余原文(含注释/顺序)不变。保存时把脏层逐个 PUT 回后端。
import { parseDocument, type Document } from 'yaml'
import { api } from '../api'

export type Path = (string | number)[]

export class EditSession {
  private docs = new Map<string, Document>()
  dirty = new Set<string>()

  /** 懒加载某层的 Document(用后端返回的源文件原文,保留注释)。 */
  async ensure(code: string): Promise<Document> {
    if (!this.docs.has(code)) {
      const d = await api.model(code)
      this.docs.set(code, parseDocument(d.raw ?? ''))
    }
    return this.docs.get(code)!
  }

  has(code: string) { return this.docs.has(code) }
  toJS(code: string): any { return this.docs.get(code)?.toJS() }

  setIn(code: string, path: Path, value: any) {
    const doc = this.docs.get(code); if (!doc) return
    doc.setIn(path, value)
    this.dirty.add(code)
  }

  deleteIn(code: string, path: Path) {
    const doc = this.docs.get(code); if (!doc) return
    doc.deleteIn(path)
    this.dirty.add(code)
  }

  /** 在某数组末尾追加一个节点。 */
  addIn(code: string, arrayPath: Path, value: any) {
    const doc = this.docs.get(code); if (!doc) return
    doc.addIn(arrayPath, doc.createNode(value))
    this.dirty.add(code)
  }

  /** 设置一个约束键;value 为空/假时删除该键,并在约束对象空了时删除整个 constraints。 */
  setConstraint(code: string, attrPath: Path, key: string, value: any) {
    const doc = this.docs.get(code); if (!doc) return
    const consPath = [...attrPath, 'constraints']
    if (value === undefined || value === null || value === '' || value === false) {
      doc.deleteIn([...consPath, key])
      const cons = doc.getIn(consPath) as any
      if (cons && typeof cons.toJSON === 'function') {
        const j = cons.toJSON()
        if (!j || Object.keys(j).length === 0) doc.deleteIn(consPath)
      }
    } else {
      if (doc.getIn(consPath) === undefined) doc.setIn(consPath, doc.createNode({}))
      doc.setIn([...consPath, key], value)
    }
    this.dirty.add(code)
  }

  // lineWidth:0 关闭自动折行,避免把未编辑的长字符串(如 description)重新折行造成无谓 diff。
  render(code: string): string { return this.docs.get(code)?.toString({ lineWidth: 0 }) ?? '' }

  async saveAll(principal: string): Promise<string[]> {
    const saved: string[] = []
    for (const code of [...this.dirty]) {
      await api.saveModel(code, this.render(code), principal)
      saved.push(code)
    }
    this.dirty.clear()
    return saved
  }

  reset() { this.docs.clear(); this.dirty.clear() }
}