Getting Started
End-to-end: install the stack, build a DocStore against a GitHub repo,
and run a transaction. The same shape works for Node CLIs, browser apps,
and React Native apps — only the adapter changes.
1. Install
pnpm add @dev-bench/docstore @dev-bench/git-fs @dev-bench/gitappFor the UI layer (optional):
pnpm add @dev-bench/gitapp-ui
# Peer deps if you don't already have them:
pnpm add react react-native react-native-svg2. Build a DocStore
createGitAppDocStore takes three things: a GitFs (storage), a
RepoManagement (REST provider), and a RepoRef (which repo / branch).
import { createBrowserGitFs } from "@dev-bench/git-fs/adapters/browser";
import { githubManagement } from "@dev-bench/git-fs/mgmt/github";
import { createGitAppDocStore } from "@dev-bench/gitapp";
const docStore = createGitAppDocStore({
gitFs: createBrowserGitFs({ dbName: "myapp" }),
mgmt: githubManagement({
proxyBase: "https://cors.example.com/github",
}),
repoRef: {
provider: "github",
owner: "octocat",
name: "notes",
branch: "main",
proxyBase: "https://cors.example.com/github",
},
// Token lookup is a function so the docstore picks up fresh tokens
// when the user re-auths without needing reconstruction.
getToken: () => ({ kind: "present", token: process.env.GITHUB_TOKEN! }),
author: {
name: "Octocat",
email: "octocat@github.com",
timestampISO: () => new Date().toISOString(),
},
});Why a CORS proxy? Browsers can't talk to
github.comdirectly for git smart-http or REST. In Node, swapcreateBrowserGitFsforcreateNodeGitFsand dropproxyBase.
3. Read
import { match } from "ts-pattern";
const r = await docStore.read("todos/1.md");
match(r)
.with({ kind: "found" }, ({ content, versionToken }) => {
console.log(content);
return versionToken; // pass back as ifMatch for safe updates
})
.with({ kind: "missing" }, () => console.log("no such doc"))
.exhaustive();Reads never throw on user-recoverable conditions; they return a
discriminated union. Same for list, runTransaction, syncIn.
4. Write a transaction
const result = await docStore.runTransaction({
mutations: [
{
kind: "put",
path: "todos/1.md",
content: "# buy milk\n\nstatus: open\n",
ifMatch: { kind: "absent" }, // create-only; fails if path exists
},
],
message: "todo: add buy milk",
onConflict: { kind: "rebase-mutations", maxAttempts: 3 },
});
match(result)
.with({ kind: "ok" }, ({ commitOid }) => console.log("ok", commitOid))
.with({ kind: "conflict" }, ({ paths }) => console.warn("conflict", paths))
.with({ kind: "rejected" }, ({ reason }) => console.warn(reason))
.with({ kind: "network" }, ({ message }) => console.warn(message))
.exhaustive();Available mutation kinds (v1):
put— create or update a doc.delete— remove a doc.json-patch— RFC 6902 patch on a JSON doc.upsert-frontmatter— merge YAML frontmatter on a markdown doc.
5. Watch
const unsub = docStore.watch("todos/", (event) => {
// Fires on remote-driven changes only — local writes don't echo.
console.log("remote changed:", event);
});
await docStore.syncIn(); // force a pull-then-emit cycle
unsub();6. UI (optional)
@dev-bench/gitapp-ui provides view-model-driven components — every
async control surfaces idle / loading / ready / error explicitly.
import { ThemeProvider, defaultLightTheme, ConnectionPanel } from "@dev-bench/gitapp-ui";
export function App() {
return (
<ThemeProvider theme={defaultLightTheme}>
<ConnectionPanel model={connectionModel} />
</ThemeProvider>
);
}The component takes a connectionModel (a thin view-model the host app
implements). See API for the full set.
Tests
For unit tests, swap the adapter:
import { createInMemoryGitFs, createInMemoryRemote } from "@dev-bench/git-fs/adapters/in-memory";
const remote = createInMemoryRemote("test-remote");
const gitFs = createInMemoryGitFs({ remote });
// ... build docStore as above; no network involved.