useOfflineBookingCachePolling.js 13.7 KB
/**
 * @description: 轮询离线预约缓存
 */

import Taro from '@tarojs/taro'
import { refresh_offline_booking_cache } from '@/composables/useOfflineBookingCache'
import { get_network_type, is_usable_network } from '@/utils/network'

/**
 * @description: 轮询状态
 * @typedef {Object} PollingState
 * @property {Number} timer_id 轮询定时器id
 * @property {Boolean} running 是否正在轮询
 * @property {Boolean} in_flight 是否正在刷新
 * @property {Number} ref_count 引用计数
 * @property {Boolean} app_enabled 是否启用应用
 * @property {Object} last_options 最后一次选项
 * @property {Boolean} network_usable 网络可用性
 * @property {Boolean} has_network_listener 是否已注册网络监听器
 * @property {Function} network_listener 网络监听器
 * @property {Promise} network_listener_promise 网络监听器Promise
 *
 * 状态同步规则(app_enabled 与 ref_count):
 * - app_enabled = true 时,ref_count >= 1(至少有一个使用者)
 * - app_enabled = false 时,ref_count = 0(无使用者)
 * - enable_offline_booking_cache_polling 会设置 app_enabled = true
 * - disable_offline_booking_cache_polling 会设置 app_enabled = false
 * - acquire_polling_ref 会增加 ref_count
 * - release_polling_ref 会减少 ref_count,降为0时触发清理
 */

/** @type {PollingState} */
const polling_state = {
  timer_id: null, // 轮询定时器id
  running: false, // 是否正在轮询
  in_flight: false, // 是否正在刷新
  ref_count: 0, // 引用计数
  app_enabled: false, // 是否启用应用
  last_options: null, // 最后一次选项
  network_usable: null, // 网络可用性
  has_network_listener: false, // 是否已注册网络监听器
  network_listener: null, // 网络监听器
  network_listener_promise: null // 网络监听器Promise
}

/**
 * @description: 规范化选项参数(纯函数,无副作用)
 * @param {Object} options 选项
 * @return {Object} 规范化后的选项
 */
const normalize_options = options => {
  return options || {}
}

/**
 * @description: 保存最后一次选项(用于网络恢复时重启轮询)
 * @param {Object} options 选项
 * @return {Object} 保存后的选项
 */
const save_last_options = options => {
  if (options) {
    polling_state.last_options = options
  }
  return polling_state.last_options
}

/**
 * 这是异步编程中典型的飞行状态锁(In-Flight Lock) 模式,是异步防重的核心思维落地方式;
 * 核心逻辑:执行前 “上锁” 标记 → 执行异步操作 → 无论成败都 “解锁” 重置标记,从根源避免重复执行;
 * finally 块是关键保障:防止异步操作报错导致 “永久上锁”,确保后续调用能正常执行。
 */

/**
 * @description: 刷新离线预约缓存一次
 * @param {Object} options 选项
 * @param {Boolean} options.force 是否强制刷新
 */
const run_refresh_once = async options => {
  // 前置检查:不满足轮询条件时直接返回(网络不可用或无引用)
  if (!should_run_polling()) {
    return
  }
  // 核心防重复——如果正在刷新,直接返回
  if (polling_state.in_flight) {
    return
  }
  // 标记为"正在刷新"
  polling_state.in_flight = true
  try {
    await refresh_offline_booking_cache({ force: !!options?.force })
  } finally {
    // 刷新完成后,标记为"刷新完成"
    polling_state.in_flight = false
  }
}

/**
 * @description: 更新网络可用性
 */

const update_network_usable = async () => {
  const type = await get_network_type()
  polling_state.network_usable = is_usable_network(type)
}

/**
 * @description: 判断是否需要运行轮询
 * @return {Boolean} 是否需要运行轮询
 *
 * 返回 false 的条件:
 * 1. ref_count <= 0:无使用者,无需轮询
 * 2. network_usable === false:网络不可用,无需轮询
 * 3. network_usable === null:网络状态未初始化,避免在无监听器时误判
 *
 * 返回 true 的条件:
 * 1. ref_count > 0:至少有一个使用者
 * 2. network_usable === true:网络可用
 */
const should_run_polling = () => {
  if (polling_state.ref_count <= 0) {
    return false
  }
  if (polling_state.network_usable === false) {
    return false
  }
  if (polling_state.network_usable === null) {
    return false
  }
  return true
}

/**
 * @description: 确保网络监听器已注册
 * @return {Promise<Boolean>} 是否注册成功(true=成功,false=失败)
 */
const ensure_network_listener = async () => {
  /**
   * 代码优先通过两个条件判断避免重复执行监听器逻辑
   * 1. 有已注册的监听器直接返回
   * 2. 有未完成的注册 Promise 则直接返回
   */

  if (polling_state.has_network_listener) {
    await update_network_usable()
    return true
  }

  if (polling_state.network_listener_promise) {
    await polling_state.network_listener_promise
    // 等待注册完成后检查是否成功
    return polling_state.has_network_listener
  }

  // 立即执行异步的监听器注册流程(标记状态→更新网络可用性→定义回调→注册监听)
  polling_state.network_listener_promise = (async () => {
    // 标记已注册网络监听器
    polling_state.has_network_listener = true
    // 初始化时更新网络可用性
    await update_network_usable()

    // 网络状态变化监听器, 网络状态变化时的处理逻辑,此时只是定义,不会立即执行
    polling_state.network_listener = res => {
      const is_connected = res?.isConnected !== false
      const type = res?.networkType || 'unknown'
      polling_state.network_usable = is_connected && is_usable_network(type)

      // 改进:不再主动停止轮询,由 run_refresh_once 中的 should_run_polling() 前置检查控制
      // 优势:
      // 1. 避免网络恢复时需要额外的重启逻辑
      // 2. 保持定时器稳定,避免频繁启动/停止
      // 3. 网络不可用时,刷新操作会在 run_refresh_once 中被前置检查过滤掉

      // 网络恢复时,确保轮询正在运行(处理之前因网络不可用而可能停止的轮询)
      if (polling_state.network_usable && should_run_polling()) {
        // 传入 restart: true,支持重启逻辑
        // 使用 normalize_options 显式处理 null/undefined,语义更清晰
        start_offline_booking_cache_polling({
          ...normalize_options(polling_state.last_options),
          restart: true
        })
      }
    }

    try {
      // 注册网络状态变化监听器
      Taro.onNetworkStatusChange(polling_state.network_listener)
    } catch (e) {
      polling_state.has_network_listener = false
      polling_state.network_listener = null
      polling_state.network_usable = null
      console.error('注册网络监听失败:', e)
    }
  })()

  try {
    // 等待网络监听器初始化完成
    await polling_state.network_listener_promise
  } finally {
    // 等待注册流程完成后,强制清空 Promise 缓存(finally 块),保证下次执行逻辑时状态干净
    polling_state.network_listener_promise = null
  }

  // 返回注册是否成功
  return polling_state.has_network_listener
}

/**
 * @description: 注销网络监听器
 * 涉及字段:
 * - has_network_listener:是否有注册网络监听器
 * - ref_count:引用计数
 * - network_listener:网络状态变化监听器
 * - network_usable:网络可用性状态
 */

const teardown_network_listener = () => {
  // 1. 前置校验:避免无效执行
  // 如果没有注册网络监听器,直接返回
  if (!polling_state.has_network_listener) {
    return
  }
  // 如果有引用计数,说明有其他地方在使用轮询,不能注销监听器
  if (polling_state.ref_count > 0) {
    return
  }
  // 标记监听器已注销(核心状态更新)
  polling_state.has_network_listener = false
  // 解绑框架层面的监听器
  if (polling_state.network_listener && typeof Taro.offNetworkStatusChange === 'function') {
    try {
      Taro.offNetworkStatusChange(polling_state.network_listener)
    } catch (e) {
      // 捕获解绑失败的异常(比如监听器已被手动解绑)
      console.warn('注销网络监听器失败:', e)
    }
  }
  // 手动清空本地引用(关键!无论解绑成功/失败都要做)
  // 注销后,清空网络监听器引用,确保后续调用能正常工作
  polling_state.network_listener = null
  /**
   * 核心目的:清空 network_usable = null 是为了让状态和监听器的生命周期完全同步 —— 监听器注销后,其产生的网络状态也必须失效,避免 “无监听器却有状态” 的矛盾;
   * 关键作用:通过让 should_run_polling() 直接返回 false,杜绝基于过期状态启动轮询的可能;
   * 设计思维:体现了 “状态闭环” 的工程化思想 —— 任何状态都要有明确的产生、更新、销毁逻辑,不残留 “脏数据” 干扰后续流程。
   */
  // 清空网络可用性状态,确保后续判断逻辑能正常工作
  // 清空衍生状态,避免脏数据
  polling_state.network_usable = null
}

/**
 * @description: 启动离线预约缓存轮询
 * @param {Object} options 选项
 * @param {Number} options.interval_ms 轮询间隔,单位毫秒
 * @param {Boolean} options.immediate 是否立即刷新一次
 * @param {Boolean} options.force 是否强制刷新(透传给 refresh_offline_booking_cache)
 * @param {Boolean} options.restart 是否为重启操作(网络恢复时调用)
 */
const start_offline_booking_cache_polling = options => {
  options = normalize_options(options)
  if (!should_run_polling()) {
    return
  } // 不满足轮询条件直接返回

  const interval_ms = Number(options?.interval_ms || 60000)
  const is_restart = options?.restart === true

  // 改进:区分首次启动和重启的防重逻辑
  // 首次启动时,如果已经在轮询则直接返回(防重复启动)
  // 重启时,需要清除旧定时器并重新建立(支持网络恢复时重启)
  if (polling_state.running && !is_restart) {
    return
  }

  // 如果是重启或定时器已存在,先清除旧定时器
  if (is_restart && polling_state.timer_id) {
    clearInterval(polling_state.timer_id)
    polling_state.timer_id = null
  }

  polling_state.running = true // 标记为"正在轮询"

  // 立即刷新一次,确保轮询开始时数据是最新的
  if (options?.immediate !== false) {
    run_refresh_once(options)
  }

  // 启动轮询定时器,按照指定间隔执行刷新操作
  polling_state.timer_id = setInterval(() => {
    run_refresh_once(options)
  }, interval_ms)
}

/**
 * @description: 停止离线预约缓存轮询
 */

const stop_offline_booking_cache_polling = () => {
  if (polling_state.timer_id) {
    clearInterval(polling_state.timer_id)
    polling_state.timer_id = null
  }
  polling_state.running = false
}

/**
 * 引用计数的核心作用
 * 这两个函数实现了轮询功能的 “引用计数式资源管理”,本质是追踪有多少 “使用者 / 场景” 依赖这个轮询功能,从而决定是否启动 / 维持 / 停止轮询、注册 / 注销网络监听器,
 * 核心目的是:
 * - 避免轮询被重复启动、错误停止
 * - 防止无使用者时仍占用资源(定时器、网络监听器)
 * - 保证多场景共用轮询时的逻辑一致性
 */

/**
 * @description: 增加轮询引用计数
 * 核心动作:将全局的 ref_count 加 1,代表 "又多了一个场景需要使用轮询功能"。
 * @param {Object} options 选项
 */
const acquire_polling_ref = options => {
  save_last_options(options)
  polling_state.ref_count += 1
  // 改进:检查网络监听器注册结果,只有成功后才启动轮询
  ensure_network_listener().then(success => {
    if (success && polling_state.last_options) {
      start_offline_booking_cache_polling(polling_state.last_options)
    }
  })
}

/**
 * @description: 减少轮询引用计数
 * 核心动作:将 ref_count 减 1(且保证不会为负数),代表 “有一个场景不再需要轮询功能”。
 */

const release_polling_ref = () => {
  polling_state.ref_count = Math.max(0, polling_state.ref_count - 1)
  if (polling_state.ref_count === 0) {
    // 引用计数降为0时,停止轮询并注销网络监听器
    stop_offline_booking_cache_polling()
    teardown_network_listener()
  }
}

/**
 * @description: 启用离线预约缓存轮询
 * @param {Object} options 选项
 * @param {Number} options.interval_ms 轮询间隔,单位毫秒
 * @param {Boolean} options.immediate 是否立即刷新一次
 * @param {Boolean} options.force 是否强制刷新(透传给 refresh_offline_booking_cache)
 */
export const enable_offline_booking_cache_polling = options => {
  save_last_options(options)
  /**
   * 核心目的:对 app_enabled=true 的场景做兜底,确保轮询在 "已启用但异常停止" 时能被主动恢复,而非被动等待网络变化;
   * 执行逻辑:先保证网络监听器(轮询的依赖)就绪,再尝试启动轮询,且利用 start_offline_booking_cache_polling 的幂等性避免重复;
   * 设计思维:体现了 "主动调用需即时生效" 的用户体验考量,以及 "依赖前置检查" 的工程化思维 —— 先保证依赖(监听器)就绪,再执行核心操作(启动轮询)。
   */
  if (polling_state.app_enabled) {
    ensure_network_listener().then(success => {
      if (success && polling_state.last_options) {
        start_offline_booking_cache_polling(polling_state.last_options)
      }
    })
    return
  }
  polling_state.app_enabled = true
  acquire_polling_ref(polling_state.last_options || {})
}

/**
 * @description: 禁用离线预约缓存轮询
 */

export const disable_offline_booking_cache_polling = () => {
  if (!polling_state.app_enabled) {
    return
  }
  polling_state.app_enabled = false
  release_polling_ref()
}