Skip to content

Instantly share code, notes, and snippets.

@andyjessop
Last active January 6, 2025 13:55
Show Gist options
  • Save andyjessop/cec5a71d885d52fab54e5b4d43fb264f to your computer and use it in GitHub Desktop.
Save andyjessop/cec5a71d885d52fab54e5b4d43fb264f to your computer and use it in GitHub Desktop.
PouchDB-CloudflareKV adapter
import { AbstractLevelDOWN } from "abstract-leveldown";
import type { AsyncMap } from "../../../../packages/async-map/src/types.ts";
type Callback<T = void> = (error?: Error | null, result?: T) => void;
/**
* A utility for enabling hook up between various "AsyncMap" storage mechanisms to PouchDB via the Level adapter
*/
export class AsyncMapLevel extends AbstractLevelDOWN<string, string> {
#storage: AsyncMap<string, string>;
constructor(storage: AsyncMap<string, string>, location?: string) {
super(typeof location === "string" ? location : "");
this.#storage = storage;
}
protected _serializeKey(key: string | Buffer): string {
// If you want pure pass-through, treat Buffers as UTF-8 or otherwise
// handle them. For plain strings, simply return the string itself.
return key.toString();
}
protected _serializeValue(value: string | Buffer): string {
return value.toString();
}
_info(callback: Callback<any>): void {
Promise.resolve().then(() => callback(null, { type: "async-map" }));
}
_open(_: any, callback: Callback<this>): void {
Promise.resolve().then(() => callback(null, this));
}
_put(key: string, value: string, _: any, callback: Callback): void {
this.#storage.set(key, value).then(() => callback());
}
_get(key: string, _: any, callback: Callback<string | Buffer>): void {
this.#storage.get(key).then((value) => {
if (value === undefined) {
callback(new Error("NotFound"));
} else {
callback(null, value);
}
});
}
_del(key: string, _: any, callback: Callback): void {
this.#storage.delete(key).then(() => callback());
}
_batch(
array: Array<{ key: string; value?: string; type: "put" | "del" }>,
_: any,
callback: Callback,
): void {
const promises = [];
for (const operation of array) {
if (!operation) continue;
const { key, value, type } = operation;
if (type === "put" && value !== undefined) {
promises.push(this.#storage.set(key, value));
} else if (type === "del") {
promises.push(this.#storage.delete(key));
}
}
Promise.all(promises).then(() => callback());
}
static destroy(_: string, callback: Callback): void {
Promise.resolve().then(() => callback());
}
}
const kv = c.env.MY_KV;
PouchDB.adapter("kv", createKvPouch(new KvAsyncMap(kv)), true);
const kvDb = new PouchDB("my-db", { adapter: "kv" });
import type { AsyncMap } from "../../../../packages/async-map/src/types";
export class KvAsyncMap<K, V> implements AsyncMap<K, V> {
private kvNamespace: KVNamespace;
constructor(kvNamespace: KVNamespace) {
this.kvNamespace = kvNamespace;
}
async clear(): Promise<void> {
const keys = await this.kvNamespace.list();
for (const key of keys.keys) {
await this.kvNamespace.delete(key.name);
}
}
async delete(key: K): Promise<boolean> {
const keyString = String(key); // Ensure the key is a string (Cloudflare KV requires string keys)
const value = await this.kvNamespace.get(keyString);
if (value === null) {
return false; // Key doesn't exist
}
await this.kvNamespace.delete(keyString);
return true;
}
async forEach(
callbackFn: (value: V, key: K, map: AsyncMap<K, V>) => void | Promise<void>,
thisArg?: any,
): Promise<void> {
const keys = await this.kvNamespace.list();
for (const key of keys.keys) {
const value = await this.kvNamespace.get(key.name);
if (value !== null) {
await callbackFn.call(thisArg, value as V, key.name as K, this);
}
}
}
async get(key: K): Promise<V | undefined> {
const keyString = String(key); // Ensure the key is a string
const value = await this.kvNamespace.get(keyString);
return value ? (value as V) : undefined;
}
async has(key: K): Promise<boolean> {
const keyString = String(key); // Ensure the key is a string
const value = await this.kvNamespace.get(keyString);
return value !== null;
}
async set(key: K, value: V): Promise<void> {
const keyString = String(key); // Ensure the key is a string
// @ts-ignore
await this.kvNamespace.put(keyString, value);
}
async size(): Promise<number> {
const keys = await this.kvNamespace.list();
return keys.keys.length;
}
}
import type { AsyncMap } from "async-map/src/types.ts";
import CoreLevelPouch from "pouchdb-adapter-leveldb-core";
import { AsyncMapLevel } from "./AsyncMapLevel.ts";
export function createKvPouch(stub: AsyncMap<string, string>) {
KVPouch.valid = () => true;
KVPouch.use_prefix = false;
function KVPouch(opts: any, callback: any) {
const _opts = Object.assign(
{
db: (location: string) => {
return new AsyncMapLevel(stub, location);
},
},
opts,
);
CoreLevelPouch.call(this, _opts, callback);
}
return KVPouch;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment