Adapters
Built-in datasource and version store adapters, and how to implement your own.
Nuska separates what to version (the datasource) from where to store version history (the version store). Both are small interfaces you can implement for any backend.
DataSourceAdapter
The interface your data store must satisfy:
interface DataSourceAdapter {
read(key: string): Promise<unknown>
write(key: string, value: unknown): Promise<void>
delete(key: string): Promise<void>
list(): Promise<string[]>
clear?(): Promise<void> // optional — used by checkout() for efficient bulk wipe
}VersionStore
The interface for persisting commits, branches, and pull requests:
interface VersionStore {
saveCommit(commit: Commit): Promise<void>
getCommit(id: string): Promise<Commit | undefined>
saveBranch(branch: Branch): Promise<void>
getBranch(name: string): Promise<Branch | undefined>
listBranches(): Promise<Branch[]>
deleteBranch(name: string): Promise<void>
savePullRequest(pr: PullRequest): Promise<void>
getPullRequest(id: string): Promise<PullRequest | undefined>
listPullRequests(): Promise<PullRequest[]>
listPullRequestsPaginated?(
opts: PaginationOptions & { status?: PullRequest["status"] }
): Promise<PaginatedResult<PullRequest>>
}listPullRequestsPaginated is optional — implement it if you want cursor-based pagination on PR listings.
MemoryDataSourceAdapter
In-memory Map-backed datasource. Works in every environment — browser, Node.js, Bun, Deno, edge runtimes.
import { MemoryDataSourceAdapter } from "@codecanon/nuska"
const datasource = new MemoryDataSourceAdapter()Great for unit tests and demos. Data is lost when the process exits.
MemoryVersionStore
In-memory Map-backed version store. Stores commits, branches, and PRs as separate Maps.
import { MemoryVersionStore } from "@codecanon/nuska"
const store = new MemoryVersionStore()IndexedDBDataSourceAdapter
Browser-native datasource backed by IndexedDB. Persists data across page reloads.
import { IndexedDBDataSourceAdapter } from "@codecanon/nuska/adapters"
// The string argument is the IndexedDB database name
const datasource = new IndexedDBDataSourceAdapter("my-app-data")The database is opened lazily on first use. Each key/value pair is stored as { key, value } in the entries object store.
IndexedDBVersionStore
Browser-native version store backed by IndexedDB. Uses three object stores: commits, branches, and pullRequests.
import { IndexedDBVersionStore } from "@codecanon/nuska/adapters"
const store = new IndexedDBVersionStore("my-app-versions")Implements listPullRequestsPaginated with an index on createdAt for efficient cursor pagination.
Implementing a custom adapter
PostgreSQL example
import type { DataSourceAdapter } from "@codecanon/nuska"
import { sql } from "./db" // your query client
class PostgresDataSourceAdapter implements DataSourceAdapter {
async read(key: string) {
const [row] = await sql`SELECT value FROM kv WHERE key = ${key}`
return row?.value ?? undefined
}
async write(key: string, value: unknown) {
await sql`
INSERT INTO kv (key, value) VALUES (${key}, ${JSON.stringify(value)})
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
`
}
async delete(key: string) {
await sql`DELETE FROM kv WHERE key = ${key}`
}
async list() {
const rows = await sql`SELECT key FROM kv`
return rows.map((r) => r.key as string)
}
async clear() {
await sql`TRUNCATE kv`
}
}MongoDB example
import type { VersionStore, Commit, Branch, PullRequest } from "@codecanon/nuska"
import { db } from "./mongo" // your MongoClient database
class MongoVersionStore implements VersionStore {
private commits = db.collection<Commit>("nuska_commits")
private branches = db.collection<Branch>("nuska_branches")
private prs = db.collection<PullRequest>("nuska_prs")
async saveCommit(c: Commit) {
await this.commits.replaceOne({ id: c.id }, c, { upsert: true })
}
async getCommit(id: string) {
return (await this.commits.findOne({ id })) ?? undefined
}
async saveBranch(b: Branch) {
await this.branches.replaceOne({ name: b.name }, b, { upsert: true })
}
async getBranch(name: string) {
return (await this.branches.findOne({ name })) ?? undefined
}
async listBranches() {
return this.branches.find().toArray()
}
async deleteBranch(name: string) {
await this.branches.deleteOne({ name })
}
async savePullRequest(pr: PullRequest) {
await this.prs.replaceOne({ id: pr.id }, pr, { upsert: true })
}
async getPullRequest(id: string) {
return (await this.prs.findOne({ id })) ?? undefined
}
async listPullRequests() {
return this.prs.find().sort({ createdAt: -1 }).toArray()
}
}Any adapter that satisfies the interface works. The engine only calls the methods defined above — you are free to add indexes, caching, or batching in your implementation.