OverviewFilesCommitsIssuesDocsReleasesPeers

keet-cli / keet-core-4.14.4-pretty / lib / core / sync / updater.js

483 lines · javascript ref:
const e = require("ready-resource"),
  t = require("safety-catch"),
  s = require("b4a"),
  i = require("./loader"),
  { KeetContract: a } = require("../contract"),
  {
    contract: {
      hyperschema: { getEnum: o },
    },
  } = require("../../../schema/spec"),
  {
    verifyIdentityProofs: n,
    generateRoomCapabilities: r,
    generateIdentityPairingName: l,
  } = require("../invitations"),
  { RoomStatus: h } = require("../contract/identity/private"),
  { KeetErrorV2: c } = require("../../errors"),
  d = o("@keet/room-type"),
  u = "insert",
  m = 16
function f(e, t) {
  return s.equals(e.oplog.key, t.oplog.key) && e.oplog.seq === t.oplog.seq
}
module.exports = class extends e {
  constructor(e, t, s, i) {
    ;(super(),
      (this.keet = e),
      (this.swarm = e.swarm),
      (this.local = e.local),
      (this.view = t),
      (this.base = s),
      (this.changes = i),
      (this.isCurrentPrivateProfile = !1),
      (this.currentPrivateProfileInfo = null),
      (this.currentIdentityInfo = null),
      (this.currentWriterInfo = null),
      (this.currentRoomInfo = null),
      (this.roomInfo = null),
      (this.updated = !1),
      (this.updates = {
        cache: null,
        tasks: null,
        contacts: null,
        dispatches: null,
        shouldUpgrade: !1,
        profileDispatches: null,
        unreadKeys: !1,
        activeCall: null,
        activeCallDevice: null,
        activeCallSynced: !1,
        sharedRooms: null,
        sharedPreferences: null,
        pendingInvitations: null,
        startIdentitySwap: null,
        attemptIdentitySwap: null,
        finishIdentitySwap: null,
        logout: !1,
      }),
      (this.events = []),
      (this._loader = null),
      (this._running = null))
  }
  _open() {
    return ((this._loader = new i(this.local, this.view, this.base)), this._loader.ready())
  }
  async _close() {
    this._loader && (await this._loader.close())
  }
  async _syncDigest(e) {
    if ((e || (e = await this._loader.loadDigest()), e)) {
      if (!this.keet.isValidBuildType(e)) throw c.INVALID_BUILD_TYPE()
      ;(this.events.push({ type: "v2:room-digest-changed", value: e }), (this.roomInfo.digest = e))
    }
  }
  async _syncConfig() {
    const e = await this._loader.loadConfig()
    e &&
      (this.events.push({ type: "v2:room-config-changed", value: e }), (this.roomInfo.config = e))
  }
  async _syncRoomCapabilities(e) {
    ;(e || (e = await this._loader.loadCapabilities()),
      e &&
        ((this.roomInfo.seed && s.equals(this.roomInfo.seed, e.seed)) ||
          (this.events.push({ type: "v2:room-capabilities-changed", value: e }),
          (this.roomInfo.seed = e.seed),
          (this.roomInfo.capabilities = r(this.base.key, e.seed)))))
  }
  async _syncSelf() {
    const [e, t, s] = await Promise.all([
      this._loader.loadDigest(),
      this._loader.loadSelfDevice(),
      this._loader.loadSelfMember(),
    ])
    if (
      ((this.roomInfo.selfDevice = t),
      (this.roomInfo.selfMember = s),
      !e || e.settings.roomType !== d.DirectMessage)
    )
      return
    const [i, a] = await Promise.all([this._loader.loadConfig(), this._loader.loadDMMembers()])
    ;((this.roomInfo.config = i), (this.roomInfo.dm = a))
  }
  async _syncSelfMember(e) {
    if (
      (this.events.push({ type: "v2:room-member-changed", value: e }),
      !e || s.equals(e.key, this.currentIdentityInfo.publicKey))
    )
      return this._syncSelf()
  }
  async _syncDevice(e) {
    if (
      (this.events.push({ type: "v2:room-device-changed", value: e }),
      !e || s.equals(e.key, this.base.local.key))
    )
      return this._syncSelf()
  }
  _setLastMessage(e) {
    ;(this.events.push({ type: "v2:room-last-message-changed", value: e }),
      (this.roomInfo.lastMessage = e.message),
      (this.roomInfo.lastMember = e.member),
      (this.roomInfo.lastMessageReactions = e.reactions),
      (this.updates.cache = { member: e.member, device: e.device }))
    const t = this.currentRoomInfo?.lastMessage
    ;(t && f(t, e.message)) || (this.updates.chat = e.message)
  }
  async _syncLastMessage() {
    const e = await this._loader.loadLastMessage()
    e && this._setLastMessage(e)
  }
  async _syncMessage(e) {
    if (!e) return
    const [t, s] = await Promise.all([
      this._loader.loadPrimaryThread(),
      this._loader.loadMessage(e.id),
    ])
    ;(0 === e.id.thread && e.id.index === t.count - 1 && this._setLastMessage(s),
      this.events.push({ type: "v2:room-chat-changed", value: s }))
  }
  async _syncReactions(e) {
    const t = await this._loader.input.get("@keet/chat", { id: e.id })
    this.events.push({ type: "v2:room-reactions-changed", value: { id: t.oplog, reactions: e } })
  }
  _syncJoinRequests(e) {
    this.events.push({ type: "v2:room-join-requests-changed", value: e })
  }
  async _syncReadLengths() {
    const e = await this._loader.loadReadLengths()
    e?.length &&
      (this.events.push({ type: "v2:read-lengths-changed", value: e }),
      (this.roomInfo.readLengths = e))
  }
  async _syncActiveCall() {
    ;((this.updates.activeCallSynced = !0), (this.roomInfo.activeCall = null))
    const e = await this._loader.loadActiveCall()
    if (e) {
      const t = e.callKey,
        s = this.base.local.key,
        i = await this._loader.loadActiveCallDevice(t, s),
        a = i ? i.deviceKey : null
      ;((this.updates.activeCallDevice = i),
        (this.roomInfo.activeCall = { callKey: t, selfDeviceKey: a }))
    }
    this.updates.activeCall = e
  }
  async _syncPreferences() {
    const e = await this._loader.loadPreferences()
    e && (this.roomInfo.preferences = e.status)
  }
  async _syncStats(e) {
    ;(e || (e = await this._loader.loadPrimaryThread()),
      e &&
        (this.events.push({ type: "v2:room-stats-changed", value: e }),
        (this.roomInfo.stats = e.stats)))
  }
  async _syncRoomIdentity(e) {
    ;(e || (e = await this._loader.loadIdentity()),
      e &&
        (this.events.push({ type: "v2:room-identity-changed", value: e }),
        (this.roomInfo.identity = e)))
  }
  async _syncUnreadAnchors() {
    this.updates.unreadKeys = !0
  }
  async _syncMember(e) {
    const i = [this._syncSelfMember(e)]
    this.roomInfo.dm?.length && i.push(this._syncConfig().catch(t))
    const { lastMember: a } = this.roomInfo
    ;(a && s.equals(a.key, e.key) && i.push(this._syncLastMessage().catch(t)), await Promise.all(i))
  }
  async _incrementalRoomInfoLoad(e, s) {
    const i = [],
      a = ["@keet/calls", "@keet/call-devices", "@keet/call-members"]
    for await (const { type: o, collection: n, value: r } of this._loader.input.changes({
      gte: e,
      lt: s,
    })) {
      if (!a.includes(n) && o !== u) continue
      let e = null
      switch (n) {
        case "@keet/system-digest":
          e = this._syncDigest(r)
          break
        case "@keet/room-config":
          e = this._syncConfig()
          break
        case "@keet/room-capabilities":
          e = this._syncRoomCapabilities(r)
          break
        case "@keet/members":
          e = this._syncMember(r)
          break
        case "@keet/devices":
          e = this._syncDevice(r)
          break
        case "@keet/chat":
          e = this._syncMessage(r)
          break
        case "@keet/reaction-digests":
          e = this._syncReactions(r)
          break
        case "@keet/threads":
          e = this._syncStats(r)
          break
        case "@keet-private-id/identity":
          e = this._syncRoomIdentity(r)
          break
        case "@keet/join-requests":
          e = this._syncJoinRequests(r)
          break
        case "@keet-private-id/unreads-v2":
        case "@keet-private-id/unreads":
          e = this._syncUnreadAnchors()
          break
        case "@keet/read-lengths":
          e = this._syncReadLengths()
          break
        case "@keet/calls":
        case "@keet/call-devices":
        case "@keet/call-members":
          e = this._syncActiveCall()
          break
        default:
          continue
      }
      e && (e.catch(t), i.push(e))
    }
    return (await Promise.all(i), !!this.roomInfo.digest)
  }
  async _fullRoomInfoLoad() {
    return (
      this.events.push({ type: "v2:room-fast-forward" }),
      (this.roomInfo = {
        roomKey: this.base.key,
        discoveryKey: this.base.discoveryKey,
        encryptionKey: this.base.encryptionKey,
        digest: null,
        stats: null,
        fork: 0,
        length: 0,
        timestamps: { creation: 0, latestActivity: 0, latestChatMessage: 0 },
        status: null,
        mailbox: null,
        selfDevice: null,
        selfMember: null,
        config: null,
        capabilities: null,
        identity: null,
        lastMessage: null,
        lastMember: null,
        lastMessageReactions: null,
        dm: null,
        activeCall: null,
        preferences: null,
      }),
      await Promise.all([
        this._syncDigest(null),
        this._syncRoomCapabilities(null),
        this._syncDevice(null),
        this._syncMember(null),
        this._syncStats(null),
        this._syncRoomIdentity(null),
        this._syncUnreadAnchors(),
        this._syncConfig(),
        this._syncReadLengths(),
        this._syncLastMessage(),
        this._syncActiveCall(),
        this._syncPreferences(),
      ]),
      !!this.roomInfo.digest
    )
  }
  async _loadPrivateProfileChanges() {
    let e = null
    if (this.isCurrentPrivateProfile && this.roomInfo.selfDevice) {
      const t = this.changes.get(a.DB_VIEW_NAME)
      ;((this._loader.shared = this.currentRoomInfo?.selfDevice ? t.shared : 1),
        (e = await Promise.all([
          this._loader.loadContactChanges(),
          this._loader.loadSharedRoomChanges(),
          this._loader.loadPendingInvitations(),
        ])))
    }
    e &&
      ((this.updates.contacts = e[0]),
      (this.updates.sharedRooms = e[1]),
      (this.updates.pendingInvitations = e[2]))
  }
  async _loadLocalInfo() {
    const [e, t, i] = await Promise.all([
      this._loader.loadLocalRoomInfo(),
      this._loader.loadLocalIdentityInfo(),
      this._loader.loadLocalPrivateProfileInfo(),
    ])
    ;((this.currentRoomInfo = e),
      (this.roomInfo = { ...this.currentRoomInfo }),
      (this.currentIdentityInfo = t),
      (this.currentPrivateProfileInfo = i),
      (this.isCurrentPrivateProfile = !!t && s.equals(t.privateProfileKey, this.base.key)))
    const a = this.currentIdentityInfo?.swapV2
    return (
      !a ||
      !s.equals(a.privateProfileKey, this.base.key) ||
      ((this.updates.attemptIdentitySwap = a), !0)
    )
  }
  async _load() {
    let [e, t] = await Promise.all([
      this._loadLocalInfo(),
      this.base.system.get(this.base.local.key),
    ])
    if (this.closing || !e) return !1
    if (this.updates.attemptIdentitySwap) {
      const e = this.updates.attemptIdentitySwap,
        t = e.writerKey,
        [i, a, o] = await Promise.all([
          this._loader.input.get("@keet-private-id/device-identities", { deviceKey: t }),
          this._loader.input.get("@keet/devices", { key: t }),
          this.local.get("@local/rooms", { roomKey: this.base.key }),
        ])
      if (!i || !a || !n(e, i)) return !1
      const r = i.deviceProof,
        h = l(e.publicKey),
        c = await this.keet.store.createKeyPair(h)
      if (!s.equals(c.publicKey, e.devicePublicKey)) return !1
      ;((this.isCurrentPrivateProfile = !0),
        (this.currentPrivateProfileInfo = o),
        (this.currentIdentityInfo = this.currentIdentityInfo.swapV2),
        (this.updates.finishIdentitySwap = {
          publicKey: this.updates.attemptIdentitySwap.publicKey,
          privateProfileKey: this.base.key,
          deviceKeyPair: c,
          deviceProof: r,
          status: { isConfirmed: !0 },
        }))
    }
    this.currentWriterInfo = t
    let i = null
    if (this.changes && this.currentRoomInfo) {
      const e = this.changes.get("_system")
      if (e.to - e.shared < m) {
        const e = this.changes.get("keet/v3/db")
        i = { from: e.shared, to: e.to }
      }
    }
    if (
      ((e = !1),
      (e = i ? await this._incrementalRoomInfoLoad(i.from, i.to) : await this._fullRoomInfoLoad()),
      !e)
    )
      return !1
    ;(this.events.push({ type: "v2:room-changed" }),
      (this.roomInfo.length = this._loader.core.length),
      (this.roomInfo.fork = this._loader.core.fork),
      this.isCurrentPrivateProfile && (await this._loadPrivateProfileChanges()))
    const a = !!this.roomInfo.selfMember?.status.isRemoved,
      o = !!this.roomInfo.selfMember?.status.isActive,
      r = !!this.roomInfo.selfDevice?.status.isActive,
      h = a || (!r && !!this.currentWriterInfo),
      c = this.base.writable && r && o,
      d = !!this.roomInfo.identity
    return (
      (this.roomInfo.status = { isWritable: c, isReadable: !0, isPrivateProfile: d, isRemoved: h }),
      (this.roomInfo.timestamps.latestSync = Date.now()),
      (this.roomInfo.timestamps.creation = this.roomInfo.digest.creationTime),
      this.roomInfo.lastMessage &&
        (this.roomInfo.timestamps.latestChatMessage = this.roomInfo.lastMessage.timestamp),
      !0
    )
  }
  _shouldUpgrade() {
    return (
      (this.roomInfo.selfMember.status.isModerator &&
        !(this.roomInfo.digest.abi.version >= this.keet.abi.autoupgrade)) ||
      null
    )
  }
  _generateMemberUpdate() {
    const e = this.currentPrivateProfileInfo.selfMember,
      t = this.roomInfo.selfMember,
      s = t.profile.timestamp > 0,
      i = e.profile.timestamp <= t.profile.timestamp
    if (!e?.profile || (s && i)) return null
    const a = { identity: this.currentIdentityInfo, privateProfile: this.currentPrivateProfileInfo }
    return { type: "@keet/update-member", op: { member: this.keet.internalGetMemberUpdate(a) } }
  }
  _generatePrivateProfileDispatches() {
    ;((this.updates.profileDispatches = []),
      this.updates.profileDispatches.push({
        type: "@keet-private-id/update-room-status",
        op: { roomKey: this.roomInfo.roomKey, key: h.IsActive, status: { isActive: !1 } },
      }))
  }
  _generateDispatches() {
    if (
      ((this.updates.dispatches = []),
      (this.updates.shouldUpgrade = this._shouldUpgrade()),
      !this.currentPrivateProfileInfo?.selfMember)
    )
      return
    const e = this._generateMemberUpdate()
    e && this.updates.dispatches.push(e)
  }
  _generateTasks() {
    if (this.updates.sharedRooms) {
      ;((this.updates.tasks = []), (this.updates.sharedPreferences = []))
      for (const e of this.updates.sharedRooms) {
        const t = e.mailbox
        if (e.identitySwapV2) {
          ;((this.updates.startIdentitySwap = {
            ...e.identitySwapV2,
            privateProfileKey: e.roomKey,
          }),
            (this.updates.tasks = [this.keet.tasks.joinRoomKeys(t, { keys: e })]))
          break
        }
        ;(e.status.isActive
          ? this.updates.tasks.push(this.keet.tasks.joinRoomKeys(t, { keys: e }))
          : this.updates.tasks.push(this.keet.tasks.leaveRoom(t, { roomKey: e.roomKey })),
          this.updates.sharedPreferences.push({ roomKey: e.roomKey, status: e.status }))
      }
      if (!this.updates.startIdentitySwap && this.updates.pendingInvitations)
        for (const e of this.updates.pendingInvitations)
          this.updates.tasks.push(this.keet.tasks.joinRoomInvitation(e.mailbox, { invitation: e }))
    }
  }
  async _maybeUpdateOpenRooms(e) {
    try {
      const t = this.currentRoomInfo?.selfMember?.profile.timestamp || 0
      if (!this.roomInfo?.selfMember || t >= this.roomInfo.selfMember.profile.timestamp) return
      const i = {
          profile: this.roomInfo.selfMember.profile,
          attestation: {
            memberKey: this.roomInfo.selfMember.key,
            proof: this.roomInfo.selfMember.proofs.profileAttestation,
          },
        },
        a = []
      for (const t of this.keet.rooms.values())
        s.equals(t.key, e) || a.push(t.dispatch("@keet/update-member", { member: i }))
      await Promise.allSettled(a)
    } catch (e) {
      this.keet.onerror(e)
    }
  }
  async _run() {
    if ((this.opened || (await this.ready()), !(await this._load()) || this.closing)) return !1
    ;(this.roomInfo.status.isWritable && this._generateDispatches(),
      this.isCurrentPrivateProfile &&
        (this.roomInfo.selfDevice?.status.isActive
          ? (this._generateTasks(), this._maybeUpdateOpenRooms(this.base.key))
          : (this.changes.logout = !0)))
    const e = this.roomInfo.selfMember,
      t = this.currentRoomInfo?.selfMember
    return (
      e?.status.isRemoved && !t?.status.isRemoved && this._generatePrivateProfileDispatches(),
      !0
    )
  }
  run() {
    return (this._running || ((this._running = this._run()), this._running.catch(t)), this._running)
  }
}