Home Reference Source

js/util/ObservableEntry.js

import Queue from './Queue.js'

/**
 * @template T
 */
export class ObservableEntry {
  /**
   * @param {T} val
   */
  constructor (val) {
    this.val = val
    this.conditions = new Set()
    this.callbacks = new Map()
  }

  set (val) {
    const changed = val !== this.val
    this.val = val

    if (changed) {
      this.testConditionsAll()
    }
  }

  get () {
    return this.val
  }

  registerCallback (condition, matchedCallback, unmatchedCallback) {
    // 있으면 있는 컨디션 이용
    if (!this.conditions.has(condition)) {
      this.conditions.add(condition)
      this.callbacks.set(condition, new Set())
    }

    // condition에 추가
    const callbackPairs = this.callbacks.get(condition)
    const callbackPair = [matchedCallback, unmatchedCallback]
    callbackPairs.add(callbackPair)

    // 취소용 함수 리턴
    return () => {
      callbackPairs.delete(callbackPair)

      // callbackPair를 가지지 않는 컨디션은 삭제
      if (callbackPairs.size > 0) return
      this.callbacks.delete(callbackPairs)
      this.conditions.delete(condition)
    }
  }

  testConditionsAll () {
    for (const condition of this.conditions.values()) {
      this.testCondition(condition)
    }
  }

  testCondition (condition) {
    const matched = condition(this.val)
    for (const [matchedCallback, unmatchedCallback] of this.callbacks.get(condition)) {
      if (matched) {
        matchedCallback(this.val)
      } else {
        unmatchedCallback(this.val)
      }
    }
  }
}

function waitToBeDefined (val) {
  return val !== undefined
}

function waitToBeChanged () {
  return true
}

export class WaitEntry {
  /**
   * @param {object} param
   * @param {ObservableEntry} param.observableEntry
   * @param {Function} param.unmatchedCallback
   * @param {boolean} param.once
   */
  constructor ({ observableEntry, unmatchedCallback = () => {}, once = true }) {
    this.promiseCallbacks = new Queue()
    this.resolved = false
    this.once = true
    this.cancel = () => {}

    this.observableEntry = observableEntry
    this.unmatchedCallback = unmatchedCallback
    this.once = once
  }

  // thenable 구현
  // reject 할일 없으므로 두번째 인수 안받음
  then (fulfill) {
    this.promiseCallbacks.push(fulfill)
    if (this.resolved) this.resolve()
  }

  resolve (newVal) {
    // once: false면 콜백 비우지 X
    // while (this.promiseCallbacks.size > 0) {
    //   const callback = this.promiseCallbacks.pop();
    //   callback(newVal);
    // }
    for (const callback of this.promiseCallbacks) {
      callback(newVal, this.cancel)
    }

    if (this.once) {
      this.promiseCallbacks.flush()
    }
  }

  // 사용 가능한 컨디션들
  toFulfill (condition, checkImmediate = true) {
    const conditionMetCallback = newVal => {
      this.resolve(newVal)

      if (this.once) {
        this.cancel()
        // promise처럼 작동할때처럼 필요, once === false인 경우 항상 resolved === false
        this.resolved = true
      }
    }

    if (checkImmediate) {
      const currentVal = this.observableEntry.val
      if (condition(currentVal)) conditionMetCallback(currentVal)
    }

    this.cancel = this.observableEntry.registerCallback(condition, conditionMetCallback, () => {
      this.unmatchedCallback()
    })

    // then() 가능하게 하기 위해서
    return this
  }

  toBe (expectedVal) {
    return this.toFulfill((val) => val === expectedVal)
  }

  toBeDefined () {
    return this.toFulfill(waitToBeDefined)
  }

  toBeChanged () {
    return this.toFulfill(waitToBeChanged, false)
  }

  /**
   * @deprecated
   */
  onChange (callback) {
    this.toBeChanged().then(callback)
  }
}

export function wait (observableEntry) {
  return new WaitEntry({ observableEntry })
}

export function observe (observableEntry) {
  return new WaitEntry({ observableEntry, once: false })
}

export function waitAll (waitEntriesFn) {
  let resolveFn
  const promise = new Promise(resolve => {
    resolveFn = resolve
  })

  // 각 waitEntry 별 resolve 여부 나타냄
  /**
   * @type {Map<WaitEntry, boolean>}
   */
  const waitEntries = new Set()
  let fulfilledWaitEntries = 0

  // waitEntry 받고...
  waitEntriesFn(observableEntry => {
    let resolved = false
    const waitEntry = new WaitEntry({
      observableEntry,
      once: false,
      unmatchedCallback: () => {
        if (!resolved) return

        resolved = false
        fulfilledWaitEntries--
      }
    })

    // 맞으면 fulfilledWaitEntry 추가
    waitEntry.then(() => {
      if (resolved) return

      resolved = true
      fulfilledWaitEntries++
      if (fulfilledWaitEntries === waitEntries.size) {
        // 정리
        for (const entry of waitEntries.values()) {
          entry.cancel()
        }
        resolveFn()
      }
    })

    waitEntries.add(waitEntry)
    return waitEntry
  })

  return promise
}