@convex-dev/geospatial
Version:
A geospatial index for Convex
51 lines • 1.95 kB
JavaScript
import { xxHash32 } from "./xxhash.js";
// We assume that (_id, key) is globally unique, so we can implement a probabilistic counter
// by incrementing and decrementing whenever `hash(_id + key) % SAMPLING_RATE == 0`. This has
// a few nice properties:
// - We uniformly sample across the increments/decrements of each key, not the keys themselves.
// So, many concurrent increments and decrements to the same key won't contend.
// - The counter never goes negative, since we use a deterministic hash.
export const SAMPLING_RATE = 1024;
export async function increment(ctx, _id, key) {
if (xxHash32(_id + key) % SAMPLING_RATE !== 0) {
return;
}
const existing = await ctx.db
.query("approximateCounters")
.withIndex("key", (q) => q.eq("key", key))
.first();
if (existing) {
await ctx.db.patch(existing._id, { count: existing.count + 1 });
}
else {
await ctx.db.insert("approximateCounters", { key, count: 1 });
}
}
export async function decrement(ctx, _id, key) {
if (xxHash32(_id + key) % SAMPLING_RATE !== 0) {
return;
}
const existing = await ctx.db
.query("approximateCounters")
.withIndex("key", (q) => q.eq("key", key))
.first();
if (!existing || existing.count === 0) {
throw new Error(`Invariant failed: Missing counter for key ${key}`);
}
if (existing.count === 1) {
await ctx.db.delete(existing._id);
}
else {
await ctx.db.patch(existing._id, { count: existing.count - 1 });
}
}
export async function estimateCount(ctx, key) {
const existing = await ctx.db
.query("approximateCounters")
.withIndex("key", (q) => q.eq("key", key))
.first();
const count = existing?.count ?? 0;
// Break ties between keys by their xxhash.
return count * SAMPLING_RATE + (xxHash32(key) % SAMPLING_RATE);
}
//# sourceMappingURL=approximateCounter.js.map