The Deno Team are building a unique product. It's an all-javascript development experience with very simple deployments:
Deno KV is a key-value store built into Deno runtime and Deno Deploy.
const kv = await Deno.openKv();
const { value } = await kv.get(["example_view_counter"]);
// Adds 1 to view counter
await kv.set(["example_view_counter"], value + 1);
console.log("π");
Deno KV is simple. But imagine two pages read 5
at the same time. They'd both write 6
. The counter would only go up by 1 for two views!
You could use Deno's atomic sum
operation.
const kv = await Deno.openKv();
// Adds 1 to view counter
await kv.atomic()
.mutate({
type: "sum",
key: ["example_view_counter"],
value: new Deno.KvU64(1n),
})
.commit();
That covers simple operations, and perhaps Deno KV is meant to be used with many simple read/write ops. But what about updating complex objects?
Deno KV inspired me to make Deno KV Plus! It's a wrapper around Deno KV that adds atomic operations for complex objects.
import { withSafeAtomics } from "https://deno.land/x/kvp/mod.ts";
// Create a KV instance with atomic support
const kv = withSafeAtomics(await Deno.openKv());
// Let's increment a view counter? Deno KV already has atomic counters.
await kv.setSafeAtomic(["view_counter"], (value) => value + 1);
// So let's try a more complex example
export async function giveGift(userId, giftName) {
await kv.setSafeAtomic(["users", userId], (userData = { items: [] }) => {
userData.items.push(giftName);
return userData;
});
}
// Now we can call it many times at the same time
await Promise.all([
giveGift("Shona", "π"),
giveGift("Shona", "π"),
giveGift("Shona", "π"),
]);
// Deno KV Plus __guarantees__ the user gets all three gifts!
Deno KV Plus simplifies handling race conditions and ensures atomic updates without manual retries. Using setSafeAtomic
, it fetches and recalculates the freshest data, guaranteeing accurate calculations. Additionally, it can abort the update process if values change and the update becomes invalid.
Yes. Let's say you have an object like { items: [] }
living at key ["users", "<username>"]
.
You could take out the items into key ["users", "<username>", "items"]
. Then you could use Deno KV's atomic operations to add items to the array.
const kv = await Deno.openKv();
// Add a new π to Shona's items. If one is already present, its value will become 2, 3, 4 etc.
await kv.atomic().mutate({
type: "sum",
key: ["users", "Shona", "items", "π"],
value: new Deno.KvU64(1n),
}).commit();
// List Shona's items
const items = await kv.list({ prefix: ["users", "Shona", "items"] });
for await (const { value: item } of items) {
console.log(item); // Prints "π"
}
To delete items in that scenario, you could use two atomic operations:
This is because before you send your transaction to take away 1 π, someone else could have added 1 π. So you need to check how many π there are before you take one away.
In Deno KV Plus, you can just modify the javascript object.
Deno KV Plus is great for KV databases that store complex or nested objects in one bucket- but that doesn't mean you should keep complex data structures in one bucket. It depends on the context.
You know what's awesome? I only had time to write a library- and thoroughly document it- because I wasn't spending time trying to get Deno KV to work! It's a wonder.