Managing a list of objects inside a key-value entry

Store items such as todos, tasks or saved records as an object keyed by id in one entry, so changing or removing one of them is a single write instead of rewriting the whole value.


Use this when items are edited or deleted after they are written, such as a todo list, a task board, saved records, or a set of settings. Keying by id is what makes a single item addressable; editing and deleting both fall out of that. The whole value is still read with a single get().

Key the items by id

Store the items as an object keyed by id, not as an array. Object paths are addressable, so every operation becomes a single round trip that never reads the list first:

// Add
await puter.kv.update('todos', {
    [id]: { text: 'Buy milk', done: false, at: Date.now() },
});

// Mark done, changing one field of one item
await puter.kv.update('todos', { [`${id}.done`]: true });

// Delete
await puter.kv.remove('todos', id);

// Show
const todos = Object.values(await puter.kv.get('todos') ?? {});

Each of those is one call with no read-modify-write, which is what an array cannot give you: array elements are not path-addressable, so changing one entry means reading the whole list, editing it in memory, and writing it back.

That also makes concurrent edits safe. Two tabs marking different todos done write disjoint paths, so both land. The read-modify-write version has a genuine race: both read the list, both write it back, and one of the two updates is lost.

A complete todo list

<script src="https://js.puter.com/v2/"></script>
<script>
const KEY = 'todos';

// Ids become dot-separated path segments, so they must not contain dots, and
// numeric-looking ids risk being read as an index. A UUID is safe.
const newId = () => crypto.randomUUID();

async function addTodo(text) {
    const id = newId();
    await puter.kv.update(KEY, { [id]: { text, done: false, at: Date.now() } });
    return id;
}

async function setDone(id, done) {
    await puter.kv.update(KEY, { [`${id}.done`]: done });
}

async function deleteTodo(id) {
    await puter.kv.remove(KEY, id);
}

async function listTodos() {
    const todos = await puter.kv.get(KEY) ?? {};
    // Key order is not guaranteed, so sort on an explicit field.
    return Object.entries(todos)
        .map(([id, todo]) => ({ id, ...todo }))
        .sort((a, b) => a.at - b.at);
}

(async () => {
    const id = await addTodo('Buy milk');
    await addTodo('Water the plants');
    await setDone(id, true);

    console.log(await listTodos());
})();
</script>

Ordering

A map has no inherent order, and the stored field order is not preserved on read. Carry an explicit at (or order) field on each item and sort when you render, as listTodos() does above. At sizes that fit in one entry this costs nothing measurable.

Notes

← All recipes