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.