Keep a Version History


Lots of editors let you go back to an earlier version, like the version history in Google Docs. With puter.kv, you can do this by saving a copy of the document under a numbered key every time it's saved. Old copies can delete themselves after a while.

This recipe uses notes, with three kinds of keys per note:

Key Holds
note:<id> The current version
note-history:<id>:00000007 A copy of version 7. Each save adds one
note-revision:<id> The latest version number

Save a Version

Each save gets the next version number, then writes the current version and a copy in one call:

const versionKey = (noteId, number) =>
    `note-history:${ noteId }:${ String(number).padStart(8, '0') }`;

async function saveNote (noteId, text) {
    const number = await puter.kv.incr(`note-revision:${ noteId }`);
    const in30Days = Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30;

    await puter.kv.set([
        { key: `note:${ noteId }`, value: { text, version: number } },
        {
            key: versionKey(noteId, number),
            value: { text, number, savedAt: Date.now() },
            expireAt: in30Days,
        },
    ]);
    return number;
}

puter.kv.incr() gives every save its own number, even when two tabs save at once. The number is padded with zeros so the keys sort in order (otherwise 10 would come before 9).

Passing an array to puter.kv.set() writes both keys in one request. The copy gets an expireAt, a Unix time in seconds, so it's deleted after 30 days. The current version has no expiry, so the note itself never goes away.

If two tabs save at the same moment, both versions end up in the history, and whichever save arrived last becomes the current version.

Show the History

To list a note's versions, call puter.kv.list() with the note's prefix. reverse: true puts the newest first, and limit loads one page at a time:

const page = await puter.kv.list({
    pattern: `note-history:${ noteId }:`,
    returnValues: true,
    reverse: true,
    limit: 20,
});

for ( const { value } of page.items ) {
    console.log(value.number, new Date(value.savedAt), value.text);
}

If there are more versions, the page includes a cursor. Pass it back to load the next page of older versions, for example from a "Show older" button:

const older = await puter.kv.list({
    pattern: `note-history:${ noteId }:`,
    returnValues: true,
    limit: 20,
    cursor: page.cursor,
});

The cursor remembers the direction, so you don't need to pass reverse again. Expired copies are skipped, so a page can come back with fewer than limit items even when there are more. Add fetchUntilFull: true if you want full pages. Store a Large Collection covers paging in more detail.

Restore a Version

To restore a version, read its copy and save it again:

async function restoreVersion (noteId, number) {
    const version = await puter.kv.get(versionKey(noteId, number));

    if ( version !== null ) {   // null once the copy has expired
        await saveNote(noteId, version.text);
    }
}

Restoring adds a new version on top instead of deleting the newer ones, a bit like git revert. If the user changes their mind, nothing is lost.

To undo the last save, restore the version before it:

const { items } = await puter.kv.list({
    pattern: `note-history:${ noteId }:`,
    returnValues: true,
    reverse: true,
    limit: 2,
});

if ( items.length === 2 ) {
    await saveNote(noteId, items[1].value.text);
}

Keep Only the Last Few Versions

Instead of expiring copies by age, you can keep a fixed number of them. Version numbers go up by one, so after saving version 57 with 50 kept, delete version 7:

const KEEP = 50;

const number = await saveNote(noteId, text);

if ( number > KEEP ) {
    await puter.kv.del(versionKey(noteId, number - KEEP));
}

If you do this, drop the expireAt from saveNote(). If a delete fails, that one copy just stays around.

Delete a Note and Its History

There's no call to delete every key with a prefix, so list them and delete them one at a time. stream: true reads the list a page at a time:

for await ( const page of puter.kv.list({ pattern: `note-history:${ noteId }:`, stream: true }) ) {
    for ( const key of page.items ) {
        await puter.kv.del(key);
    }
}

await puter.kv.del(`note:${ noteId }`);
await puter.kv.del(`note-revision:${ noteId }`);

It's safe to delete keys while you're still listing them.

Notes

← All recipes