Skip to main content

Emulating Storage API

I’ve released an new version for a library which is an emulator for Storage API, commonly known as localStorage (at one of its usages). It’s a tricky and problematic API. Also, you probably have no valid reason to use this library. However, I encountered some anecdotes worth mentioning during its development.

For those who are unfamiliar, localStorage is a key-value store that allows you to save only string values. This data persists between sessions and is synchronized across open tabs for a given origin.

Storage API has getItem and setItem methods, but what makes it tricky is that the key you set also accessible as a property on the instance itself, which allows the code in the image to work. (There are some edge cases of incompatibility between browsers):

localStorage getItem and setItem vs property accessor

Because of the nature of the Storage API, it requires working with JavaScript Proxies, which always tricky. One thing I’ve noticed is that a single action can sometimes trigger multiple proxy handlers. For example, getOwnPropertyDescriptors call will trigger both the ownKeys and getOwnPropertyDescriptor handlers.

unexpected proxy handler firing

The Storage API has a unique feature, it is one of the few APIs on the web platform features that explicitly use integer overflow. This occurs in the Storage#key() method. If you try to access an index beyond the 32bit, it wraps around and returns the item at index 0.

index overflowed test in WPT
From Web Platform Tests - storage_key.window.js

To verify API compliance, I used the Web Platform Tests (WPT) that aren’t compatible with the Vitest API. Since manual conversion was time consuming, I used AI. It took me a while to find a prompt that worked well with Gemini 2.5 Pro. Retroactively, I was able to create a prompt that works well with the cheaper version Gemini 2.5 Flash without causing any hallucinations. I discovered that sometimes “less context” is better. One thing that really helped was removing any reference to Vitest and instead providing specific instructions for each conversion needs to be done.

I give you a code snippet in JavaScript.

- Please don’t code comments if they aren’t exist in the code I gave you
- Please don’t convert `var` to `const`
- `BrowserStorage` is a global. No need to "implement it"
- `var storage = window[name];` should be converted to `new BrowserStorage()`
- If you see `Storage.prototype`. Instead of using `BrowserStorage`. Make an
  inheritance: `class TestStorage extends BrowserStorage {}`, and patch its
  `prototype` instead of `Storage`
- If you see `this.add_cleanup(…)` call. Ignore it's not needed
- The `test(callback, title)` should be converted to `test(title, callback)`
- The `assert_equals(val1, val2, message)` should be converted to
  `expect(val1, message).toBe(val2)`
- The `assert_true(expression_or_value)` should be converted to
  `expect(expression_or_value).toBe(true)`
- The `assert_false(expression_or_value)` should be converted to
  `expect(expression_or_value).toBe(false)`
- The `assert_throws_exactly(error, callback)` should be converted to
  `expect(callback).toThrowError( expect.objectContaining(error))`
- The `assert_array_equals(val1, val2, message)` should be converted to
  `expect(val1, message).toStrictEqual(val2)`
- There is a usage of global `name`, you can instead convert it to `"storage"`

Please convert the code:

<code></code>

Another conversion problem was that some tests depend on sloppy mode. Something rarely happens in development nowadays. Vite and Vitest always run in ESM, which is strict mode. Fortunately, there is an escape hatch, which is the Function constructor. Not recommended, but it was necessary to ensure compatibility

sloppy mode in strict mode