Promise单例模式

场景:用 Socket.io 和 MongoDB 做的 IM 系统,每对用户(聊天室)的聊天记录存在一个 这样的 Document 里:

{
    "name": "1&2",
    "msgs": [
        {
            "text": "...",
            "uid": "...",
            "from_id": 1,
            "to_id": 2,
            "date": "..."
        }
    ]
}
复制代码

在对聊天记录进行增删查改时,首先要获取聊天室对应的文档,如果没有这个文档,就先创建一个新文档。

function delayFactory(delay: number) {
  return new Promise(res => void setTimeout(res, delay))
}

interface Msg {
  text: string
  uid: string
  from_id: string
  to_id: string
  date: string
}

interface RoomDoc {
  name: string
  msgs: Msg[]
}

const mongo = (function () {
  const db: RoomDoc[] = []
  return {
    async getDoc(room_id: string) {
      await delayFactory(50)
      return db.find(doc => doc.name === room_id)
    },
    async createDoc(room_id: string) {
      await delayFactory(1000)
      const newRoomDoc = { name: room_id, msgs: [] }
      db.push(newRoomDoc)
      return newRoomDoc
    },
    printDB() {
      console.log(db)
    }
  }
})()

async function getRoomDoc(room_id: string) {
  return (await mongo.getDoc(room_id)) || (await mongo.createDoc(room_id))
}

getRoomDoc('1&2')
  .then(console.log)
getRoomDoc('1&2')
  .then(console.log)
  .then(mongo.printDB)
复制代码

Bug:同一个聊天室建立了多个文档。

原因:前端忘记在组件销毁时忘记Socket.off('connect'),组件反复加载后会在重连时触发多次绑定事件。后端的getRoomDoc异步函数被反复调用,存在竞态问题。

虽然主要原因是前端的低级错误,但是这种情况却是真实存在的。当网络出现波动后,同一个聊天室内的客户端可能会同时重连,出现一样的问题。


我一开始尝试把getRoomDoc变成防抖的函数,但其实防抖并不适用这种情况,因为难以设置合适的防抖时间。不如利用单例模式,让每个room_id对应的Promise<RoomDoc>在同一时间内只存在唯一实例,该Promisefulfill后会把自己从缓存中删掉。

const getRoomDocSingleton = (function () {
  const cache: Record<string, Promise<RoomDoc> > = Object.create(null)
  return function (room_id: string) {
    return cache[room_id] || (
      cache[room_id] = getRoomDoc(room_id).then(result => {
        delete cache[room_id]
        return result
      })
    )
  }
})()

getRoomDocSingleton('1&2')
  .then(console.log)
getRoomDocSingleton('1&2')
  .then(console.log)
  .then(mongo.printDB)
复制代码