Store a Set of Unique Values


Some lists should never contain the same value twice, like the posts a user liked or the tags on a note. In JavaScript you'd use a Set for this.

puter.kv has no set type, but an object works just as well. Store each value as a field name, with true as its value:

{
    'post-81': true,
    'post-102': true,
}

An object can't have the same field twice, so there are never duplicates. An array can, since puter.kv.add() appends a value even when it's already in the list.

Add a Value

To add a value, use the puter.kv.update() method with the value as the field name. It returns the whole set after the change:

const liked = await puter.kv.update('likedPosts', { [postId]: true });
// { 'post-81': true, 'post-102': true }

The first call creates the key. Adding a value that's already there changes nothing, so a double click or the same call from two tabs is harmless.

To add several values at once, put them all in the same object. One call fits about 140 short values, so split bigger changes into several calls:

await puter.kv.update('likedPosts', { 'post-7': true, 'post-9': true });

Store Values with Dots or Quotes

The field name is read as a path, where a dot means "go into a nested object". A value like a URL or v1.2 would get split at its dots. To keep it as one name, wrap it in brackets and quotes, and escape any quotes or backslashes inside it. This helper does both:

const member = (value) => `["${ value.replace(/["\\]/g, '\\$&') }"]`;

await puter.kv.update('bookmarks', { [member('https://example.com/a.html')]: true });
// { 'https://example.com/a.html': true }

Use member() for anything a user typed or that comes from outside your app. IDs from crypto.randomUUID() don't contain dots, so they work without it.

An empty string, __proto__, constructor and prototype can't be field names, even inside member(), and the call rejects with bad_request. If users can type anything, add a prefix, like member('tag:' + value).

Remove a Value

To remove a value, use the puter.kv.remove() method:

await puter.kv.remove('bookmarks', member(url));

Removing a value that isn't in the set does nothing. To remove several at once, pass each one as a separate argument. Passing the same value twice in one call rejects with bad_request, so remove duplicates first:

await puter.kv.remove('tags', ...[ ...new Set(tagsToRemove) ].map(member));

Check and List the Values

To read the set, use the puter.kv.get() method. It returns null if nothing was ever saved, and {} once every value has been removed, so default it to an empty object:

const bookmarks = await puter.kv.get('bookmarks') ?? {};

const isSaved = bookmarks[url] === true;
const urls = Object.keys(bookmarks);
const count = urls.length;

Build a Like Button

For a like button, save the state the user picked instead of flipping whatever is stored. Two tabs that each flip the stored value at the same time cancel each other out:

async function setLiked (postId, liked) {
    if ( liked ) {
        await puter.kv.update('likedPosts', { [member(postId)]: true });
    } else {
        await puter.kv.remove('likedPosts', member(postId));
    }
}

likeButton.addEventListener('click', async () => {
    const liked = likeButton.classList.toggle('liked');
    await setLiked(postId, liked);
});

Keep the Order Values Were Added

Stored objects don't keep their field order. To show the newest first, like for recent searches, store a timestamp instead of true and sort when you read:

await puter.kv.update('recentSearches', { [member(query)]: Date.now() });

const searches = await puter.kv.get('recentSearches') ?? {};
const newestFirst = Object.entries(searches)
    .sort((a, b) => b[1] - a[1])
    .map(([ query ]) => query);

Searching for something again moves it back to the top instead of adding a duplicate. To keep only the last 20:

const old = newestFirst.slice(20);

if ( old.length > 0 ) {
    await puter.kv.remove('recentSearches', ...old.map(member));
}

When to Switch

One key has a size limit that fits a few thousand short values. If the set can get bigger than that, like every post a user has ever liked, give each value its own key instead:

await puter.kv.set(`liked:${ postId }`, true);                      // add
await puter.kv.del(`liked:${ postId }`);                            // remove
const isLiked = await puter.kv.get(`liked:${ postId }`) !== null;   // check

To list them, read the keys that start with liked: a page at a time, as in Store a Large Collection.

← All recipes