Skip to content

[Bug]: storageState({ indexedDB: true }) silently empties Map and Set values #42703

Description

Version

1.64.0-next, d1ead3ecc. Reproduces identically on chromium, firefox and webkit.

Steps to reproduce

Store a Map and a Set in IndexedDB, snapshot the context, restore it into a new one:

const ctx = await browser.newContext();
const page = await ctx.newPage();
await page.goto(server.EMPTY_PAGE);
await page.evaluate(async () => {
  const db = await new Promise(res => {
    const r = indexedDB.open('probe', 1);
    r.onupgradeneeded = () => r.result.createObjectStore('s');
    r.onsuccess = () => res(r.result);
  });
  const tx = db.transaction('s', 'readwrite');
  tx.objectStore('s').put(new Map([['mk', 'mv']]), 'map');
  tx.objectStore('s').put(new Set([1, 2]), 'set');
});

const state = await ctx.storageState({ indexedDB: true });
const ctx2 = await browser.newContext({ storageState: state });
// read 'map' and 'set' back out of IndexedDB in ctx2

Expected

map comes back as a Map holding mk -> mv, set as a Set holding 1, 2. IndexedDB stores values with the structured clone algorithm, which supports both.

Actual

Both come back as empty plain objects, with no error or warning anywhere:

map -> object:{}
set -> object:{}

The loss is already visible in the snapshot itself, where both are encoded as an object with no properties:

{"key":"map","valueEncoded":{"o":[],"id":1}}
{"key":"set","valueEncoded":{"o":[],"id":1}}

Everything else I tried survives the round trip exactly, which is what made this look like an oversight rather than a deliberate subset:

num    -> number:42
str    -> string:"hello"
date   -> Date:2020-01-02T03:04:05.678Z
bytes  -> U8:1,2,250
nested -> object:{"a":[1,{"b":"c"}],"d":null}
bigint -> BigInt:9007199254740993
undef  -> undefined
map    -> object:{}      <-- was Map([['mk','mv']])
set    -> object:{}      <-- was Set([1,2])

Cause

storageScript.ts encodes records with the shared serializer in packages/isomorphic/utilityScriptSerializers.ts, and that file has explicit cases for RegExp, Date, URL, Error, ArrayBuffer, TypedArray and BigInt but none for Map or Set. With no case to match, both fall through to the generic object branch, which walks own enumerable properties. A Map has none of its contents there, so it encodes as { o: [], id: N } and parses back as {}.

Notably the types it does handle are structured clone types, so the intent looks like covering what IndexedDB can actually hold. Map and Set are structured clone types too and are just missing from the list.

Scope

I am only reporting the IndexedDB snapshot path. page.evaluate(() => new Map(...)) also yields {}, but that is the documented Serializable boundary on the evaluate return value and I assume it is intentional, so I left it out. The difference here is that storageState is a persistence feature over a store that natively holds these types, the option is pitched for real app state like Firebase auth, and nothing tells the user their data was dropped.

I am a freshman in college doing my best to be useful on real projects, so if IndexedDB snapshots are only ever meant to cover a JSON-ish subset, that would be worth a line in the option docs, and I am happy to be corrected.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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