Skip to content

Updated a nested Set in a Map leaks a DraftSet in Immer 11 #1200

@markerikson

Description

@markerikson

Per this RTK issue:

A user reported that they had a case where updating a nested Set wasn't working. They linked a repro at https://github.com/keybase/client/pull/28706/files , which has:

import {enableMapSet} from 'immer'
import {create} from 'zustand'
import {immer} from 'zustand/middleware/immer'
enableMapSet()

type Reaction = {
  timestamp: number
  username: string
}
type ReactionDesc = {
  users: Set<Reaction>
}
type Reactions = ReadonlyMap<string, ReactionDesc>

type MinimalMessage = {
  conversationIDKey: string
  ordinal: number
  reactions?: Reactions
  type: 'text'
}

type Store = {
  messageMap: Map<number, MinimalMessage>
}

type State = Store & {
  toggleLocalReaction: (p: {
    decorated: string
    emoji: string
    targetOrdinal: number
    username: string
  }) => void
}

const initialOrdinal = 1
const initialMessage: MinimalMessage = {
  conversationIDKey: 'test-convo',
  ordinal: initialOrdinal,
  type: 'text',
}

const initialMessageMap = new Map<number, MinimalMessage>()
initialMessageMap.set(initialOrdinal, initialMessage)

export const useStore = create<State>()(
  immer((set, get) => ({
    messageMap: initialMessageMap,
    toggleLocalReaction: (p: {decorated: string; emoji: string; targetOrdinal: number; username: string}) => {
      const {emoji, targetOrdinal, username} = p
      set(s => {
        const m = s.messageMap.get(targetOrdinal)
        if (m) {
          const rs = {
            users: m.reactions?.get(emoji)?.users ?? new Set(),
          }
          if (!m.reactions) {
            m.reactions = new Map()
          }
          m.reactions.set(emoji, rs)
          const existing = [...rs.users].find(r => r.username === username)
          if (existing) {
            rs.users.delete(existing)
          }
          rs.users.add({timestamp: Date.now(), username})
        }
      })
    },
  }))
)

After turning that repro into a test, I do see a DraftSet leaking:

// logging
Message:  { id: 2, reactions: Map(1) { '👍' => { users: [DraftSet [Set]] } } }

 FAIL  __tests__/rtk-5159-zustand.ts > RTK #5159 - Zustand Immer Middleware (DEVELOPMENT mode) > Pattern 1: Map/Set Operations (Keybase pattern) > add reaction to message with existing reactions
Error: [Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}
 ❯ Module.die src/utils/errors.ts:45:9
     43|   const e = errors[error]
     44|   const msg = isFunction(e) ? e.apply(null, args as any) : e
     45|   throw new Error(`[Immer] ${msg}`)
       |         ^
     46|  }
     47|  throw new Error(
 ❯ assertUnrevoked src/plugins/mapset.ts:365:23
 ❯ DraftSet.values src/plugins/mapset.ts:278:4
 ❯ DraftSet.[Symbol.iterator] src/plugins/mapset.ts:306:16
 ❯ __tests__/rtk-5159-zustand.ts:139:24

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions