Developer deep-dive page: object-store idea + WAL internals #191
Labels
No labels
actions
bug
cli
duplicate
enhancement
fork
forum
git storage
help wanted
insights
invalid
issues
moderation
oidc
ownership transfer
packages
pr/merge protection rules
projects
pull requests
question
releases
sponsorships
tags
webhooks
wiki
wontfix
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
crueber/walhub#191
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Developer deep-dive page: the object-store idea + what the WAL is and how it works
A second marketing page (route TBD in planning — e.g.
/how-it-works) that fully explains, for developers: the original idea behind storing git repos in object stores, what the "WAL" is, and how it works in detail. Reuses the generated concept GIFs wherever they carry explanatory weight (plus prose + diagrams as needed).Content (planner: verify every claim against the WAL/store docs + code)
Inspiration (not copying)
https://cursor.com/blog/git-at-any-scalefor narrative shape/pacing only — our architecture, our words, our diagrams. Do not lift copy, structure, or visuals.Acceptance criteria
/.node --testgreen; browser pass both themes, zero console errors; no new deps.Deep-dive page plan — Forgejo issue crueber/walhub#191
0. Sources this plan was verified against
docs/go/02_storage_protobuf.md(§2.1 key layout, §2.2 schema, §2.4 framing, §2.5 seq semantics, §2.6 ObjectStore/CAS)docs/go/05_wal_engine.md(§5.0 consistency, §5.2 sync levels, §5.3 publish ladder, §5.4 burn protocol, §5.5 checkpoints, §5.6 replay, §5.7 remote reader)docs/go/15_testing.md(§4.8 budget table)docs/go/08_bundles.md(§8.4 creationToken, clone table)docs/features/README.md(P1–P5, "the WAL stays git-only")web/src/pages/Landing.jsx+web/src/components/ConceptGif.jsx+web/test/unit/landing.test.jsweb/src/index.jsx+web/src/App.jsx(route table + nav)docs/go/12_web_ui.md(§2.3.0 landing precedent)README.MD(L3–5 canonical language)internal/wal+internal/store(skimmed:publish.go,sync.go,checkpoint.go,remote.go,tasks.go,store.go,keys.go)Narrative-pacing inspiration ONLY (per the issue): https://cursor.com/blog/git-at-any-scale.
Our architecture, our words, our diagrams — do not lift copy, structure, or visuals.
1. Route + nav entry
/how-it-works— new static page componentweb/src/pages/HowItWorks.jsx.web/src/index.jsx:<Route path="/how-it-works" component={HowItWorks} />placed with the other top-level static routes (next to
/explore,/api,/keys).*fallback staysLanding(unchanged — safe default, no API calls;landing.test.jspins it)./already uses).web/src/App.jsxsite nav:<A href="/how-it-works">how it works</A>(lowercase to match existing
explore/import/keys/setuplabels), placed afterexplore."How it works →" deep-linking to
#push/#wal/#fetchanchors); the deep-dive links backto
/("← walhub in 30 seconds") and to/setup. Repo-level/waldashboard links to itas "what am I looking at?" help (optional, single link — the Wal tab itself is unchanged).
2. Page outline (section by section, with technical claims + traces)
The page is a static component (like
Landing.jsx): sections +ConceptGifreuses + inlinestatic diagrams (see §3). Zero API calls, zero SDK import, zero
fetch— same rule as/.2.0 Hero — "How walhub works"
instances are disposable ("wipe one and you lose nothing but warmth").
behavior follow walgit's formats) — same one-liner as Landing L65–68.
#idea,#wal,#push,#read,#checkpoints,#boundaries).README.MDL3–5, L15–17.2.1 §
#idea— The original idea: git objects love dumb object storesTechnical claims (each traced):
bucket; the store needs no understanding of git. Trace:
02§2.1 (everything exceptmanifest.pb,bundles/list.pb,leases/*is immutable;wal/<checksum>.pack|.idx|.rev|.bitmap|.commit-graphcontent-addressed by trailing SHA),
store.goPutCreate discipline.repos/<owner>/<repo>/; anything that isrestart-surviving state is an object; disk and memory are caches. Any S3/GCS/filesystem works behind
one
ObjectStorecontract (CAS, conditional GET, compose, leases). Trace:02§2.1 key table +§2.6 interface;
AGENTS.mdlaw 4 ("The bucket is the repository");01_overview.mdstore row.coordination primitives — there are no locks on the store. Trace:
02§2.6–2.7 (PutCreate/PutUpdate,casUpdate; "the store is the lock — no mutex at all").round trips (budgets in §2.4 below). Trace:
AGENTS.mdlaw 6;01_overview.md§"No LIST on a hotpath";
15_testing.md§4.8.bucket(instance disappears, fresh instance connects, nothing lost) — the visualproof of claim 2. Alt/caption intact from
CONCEPT_ALTS.bucket.2.2 §
#wal— What the WAL is: the log of manifest mutationsTechnical claims:
repos/<o>/<r>/manifest.pb) is the linearization point:head_seq,min_seq,checkpointpointer,log_segmentscovering[min_seq, head_seq], the denormalizedlive pack set, settings, monotonic
revision. Trace:02§2.2Manifestschema;05§5.0 rule 1.LogEntryframes inlog/<seq:016x>.pbsegments(uvarint-length-prefixed, partial trailing frame tolerated on appendable segments).
Five entry kinds:
PUSH(pack + ref txn),COMPACT(new pack superseding old),REF_UPDATE(ref-only),
CHECKPOINT(marker),SETTINGS(history; latest rides the manifest inline).Trace:
02§2.2EntryKind/LogEntry, §2.4 framing;05§5.3.3 (settings/compact).(
PutUpdate(version)/PutCreate); a 412 is the normal contention signal, never an error.Trace:
05§5.0 rules 1–2;02§2.6 error taxonomy (PreconditionFailedis protocol-normal).manifest CAS leaves an orphan; later writers burn past it (3 probes × 100 ms, cap 8 consecutive
burns →
Corrupt). Orphans are harmless and swept after a later commit. Trace:02§2.5;05§5.4 ladder;publish.goslot-claim path.repo serializes all publishes in-process; across instances the CAS serializes commits; losers
re-sync, re-verify, retry (bounded ladder, 16 attempts). Group commit batches arrivals (5 ms window,
up to 64) so concurrent pushes share one commit. Trace:
05§5.3.1–5.3.2 + Concurrency notes;internal/wal/publish.go.old_oidmust equal current unlessall-zero creation; symbolic HEAD updates always ok); rejected pushes are transport-successes with
per-ref errors (
Seq: 0). Trace:05§5.3.2 step 3.pushGIF belongs to §2.3.commit point:
log PUT (Create) → manifest CAS (Update) → local refs apply → answer ok, with thethree failure branches (412 → delete-own-segment + retry; ambiguous error → re-read
casLanded,delete nothing). Static SVG, theme-safe (currentColor / muted classes), no animation, no new deps.
Justification: the CAS ladder is the page's central claim and no GIF covers it; a static diagram is
zero-weight and accessible (paired
<details>text fallback describing the same steps).2.3 §
#push— How a push flows, end to endWalk the numbered ladder as narrative (each step traced to
05§5.3.2):head_seq+1 …), build PUSH entries withPackRef{checksum, sizes, tier 0}+ callermeta (principal, request_id, push-options).
(
PutCreateof the segment; 412 → manifest re-read → burn-or-retry per §5.4).head_seq, extended pack set, new segment ref,revision+1).answered only at step 8, after commit. Trace:
05§5.3.2 steps 7–8;AGENTS.mdlaw 4.syncMu, refs first, then advertise; withdraw-on-failure (reset advertisedversion so the next sync replays — but still answer
ok, because the bucket is the truth).Trace:
05§5.0 rule 5 + §5.3.2 step 8 + ordering rule.Trace:
05§5.3.2 step 8.push(laptop → bucket, bucket writes then ACKs, client finishes after the ACK) —the visual for claim 6. Alt/caption intact from
CONCEPT_ALTS.push.2.4 §
#read— How a fetch/clone reads it backTechnical claims:
local
packed-refs, no packs) serves info/refs, ls-refs, bundle lists, web refs; Serve addsthe pack set this instance can hold; Full materializes everything (refused with
ErrTooLargeover budget); Objects serves-or-remote-reads for the web API. Trace:
05§5.2 table +sync.go.packed-refsrewrite (parse → map → tmp+rename), atomic across manyrefs, works before packs exist. Annotated-tag
^{}comes from writer-recordednew_peeled(max 16 hops), so replicas advertise without objects. Trace:
05§5.2 step 2 + §5.3.3 peeling.(manifest GET → checkpoint refs ∥ tail); push ≤ 5 (freshness GET → pack PUTs ∥ log PUT →
manifest CAS); checkpoint = 4 (never a log GET — provenance rides the applied state).
Trace:
15_testing.md§4.8 budget table (exact numbers + counting rule).bundles/list.pbadvertises full + incrementalbundles with
creationToken = slot epoch; git downloads bundles then fetches the remainder.Trace:
08§§8.1/8.4 + clone table;internal/bundle.mount or no local copy) with the block-cache remote reader + fetch-path faulter filling gaps for
the web API and fetches. Trace:
05§5.7 (+ the stated v1 decision: serve via stockgit+materialized packs + bundle-uri; no native serving engine in v1 — say this plainly).
fetch(empty clone receives ref names, then pack data). Alt/caption intact.2.5 §
#checkpoints— Checkpoints, ref snapshots, and cold startsTechnical claims:
Evaluated after publishes (background, off the reply path) and by the maintainer loop.
Trace:
05§5.5 para 1;10_maintenance.mdcheckpoint unit.checkpoint.pb∥refs.pb(PutCreate, deterministic seq-keyed) → manifest CAS(
checkpoint,min_seq = seq+1, trim folded segments,revision+1). Idempotent at equal seq;racing checkpointers resolve benignly via Create-idempotence + CAS. Trace:
05§5.5.refs.pbat the checkpoint and replays only the tail —it never replays the whole log.
min_seq = checkpoint.seq + 1; below is folded away.Trace:
05§5.5 "Cold start fold";02§2.5.refsAtSeq/refsAsOffold from the newest usable checkpoint + ordered entries;cuts older than
min_seqwith no usable checkpoint are unreplayable (why provenance timestampsexist). Trace:
05§5.6.policy.jsonis NOT on the WAL (admin API/CLI object); per-repo settings ARE published(manifest-inline +
SETTINGShistory, ≤ 16 KiB TOML, validated before publish).Trace:
02§2.1 key table;05§5.3.3publish_settings.fetch— do not triple-use it; link back to §2.4 instead).(
refs.pbsnapshot), folded-away prefix, live tail segments,min_seq/head_seqmarkers.Same static-SVG treatment as #1. Justification: "fold, don't replay" is the cold-start claim and is
inherently positional; one small figure replaces three paragraphs.
2.6 §
#boundaries— Honest boundaries: what the WAL deliberately does NOT dofamily (
orgs/…,users/…,repos/<o>/<r>/{meta,issues,checks,releases,access.json}) with itsown CAS discipline — never WAL entries, never the manifest, never gating a push (except policy
effects that explicitly consult them, e.g. required checks). Trace:
docs/features/README.mdL11–15 (the one architectural law), P1–P4.
without the collaboration layer and vice versa. (Framed as design consequence, not a metric —
no perf claim.)
(walhub stores check results, doesn't run CI), Discussions/Packages/Projects, SAML/SCIM.
Trace:
docs/features/README.mdP9.on hot paths); no cross-repo transactions (one publisher per repo; CAS is per-manifest); no native
git serving engine in v1 (stock
gitsubprocess + faulter, per05§5.7 decision); burned-seqgaps are normal, not damage.
collab(issue 7 / PR 8 / green check alongside the git lane, WAL stays git-only) —the visual proof of claim 1. Alt/caption intact from
CONCEPT_ALTS.collab.2.7 Closing — where to go next
/quickstart (push something),/explore(browse),/setup(configure), repo/waltab (watch a live manifest),
/apidocs. No claims; pure navigation.3. GIF/diagram inventory
push.gif(+-still)bucket.gif(+-still)fetch.gif(+-still)collab.gif(+-still)internal/devtools/landinggif/,make landing-gifsis manual) but all four existing GIFs already map 1:1 to sections; new GIFs add weight for no new explanatory powerReuse mechanics (copy the Landing pattern exactly):
ConceptGifcomponent (still-first,reduced-motion swap via
prefers-reduced-motion,<noscript>still,/_ui/concepts/prefix,lazy + async + 640×360, single
img altper figure — never a labeled wrapper on top).Dark-framed cards in both themes (README screenshot precedent — one asset set, no light variants).
What the #187 GIFs already cover (do NOT re-explain, link across): Landing owns the 30-second
pitch for each scene; the deep-dive links back ("the short version lives on
/") and goes onelevel deeper instead of restating.
4. Copy honesty rules
or in the plan's §2 — and the implementation change must keep the comment pins accurate
(same discipline as
landing.test.jssource pins).(
15_testing.md§4.8), never latency promises. No ms figures anywhere on the page.scale"; say materialization is refused over budget with the bundle-uri fix text, not that every
repo fits everywhere.
serving engine) is stated, not hidden; collaboration features link to their real state, not a
roadmap promise.
bucket layout, protobuf wire encoding, and git wire behavior follow walgit's formats"), never
paraphrased into something stronger.
rhythm); no lifted copy, structure, or visuals.
5. EVIDENCE / performance
/): nouseData, no SDK import, nofetch.The page spends no store round trips — asserted by a
node --testsource-pin test mirroringlanding.test.js. No new network surface, no new backend path.docs/EVIDENCE.mdentry: the page makes no hot-path performance claim (it only reportsexisting sim budgets); per
AGENTS.md, evidence entries are for transport/storage features makinghot-path claims. If a future iteration adds measured figures, it gets a harness + entry then.
runtime deps (Solid + router only, already present); no new Go modules.
6. Acceptance criteria (from the issue, made checkable)
HowItWorks.jsx+/how-it-worksroute +how it worksnav entry;*fallback still Landing.web/test/unit/how-it-works.test.js(routes, nav, anchors,alt-text pins, no
useData/SDK/fetchstrings) — mirrorslanding.test.js.02/05/08/15_testing/features/README+ theinternal/wal+
internal/storecode paths listed; traces recorded in code comments.ConceptGif(still-first + reduced-motion +noscript) used for each;
docs/go/12_web_ui.mdgains a §2.3.x subsection for the page.node --testoverweb/test/unit/*.test.jsgreen (never a directory positional — Node 22executes a directory as a module; see AGENTS.md field lessons).
chrome-cdpon :9222), both themes, zero console errors —module scripts are MIME-enforced and import-map driven ("curl says 200" proves nothing);
drive against the canonical host (loopback GETs 302 to
walgit.localhost—06_server_http.md§2.2 #2), or expect the hop.
make fmt && make vetclean;git statusshows only theintended files.
7. Open questions / risks
/how-it-worksvs/howvs/internals./how-it-worksis self-describing andmatches the issue's example; short
/howrisks collision with future pages. Recommend/how-it-works. (Needs owner sign-off — one-line decision.)?format=textparity —12_web_ui.mdnotes the text format "moves server-side with the page"for Landing (
06_server_http.md§Decisions). Does/how-it-worksneed a text rendering too, oris the SPA-only page acceptable? Recommend SPA-only for v1 (marketing page, not data), but confirm.
explore import API keys setup+how it works);check mobile wrap (existing flex-wrap handles it, but verify in the browser pass).
pacing (the cursor.blog lesson). If review finds it heavy, split boundary: §§2.1–2.3 ship first,
§§2.4–2.6 follow — but default is one page, one change.
web/public/.svgassets served under
/_ui/. Recommend inline (no extra requests, theme via currentColor) unlessthe file exceeds ~200 lines, then split to assets.
budgets 5/1/2/4). These live in config + sim tests; the page must be re-checked if they change.
Mitigation: trace comments name the exact doc sections so the next editor finds them.
fallback is one new generator-backed GIF (tooling exists,
make landing-gifsmanual). Count staysminimal by default.
8. Implementation sketch (for the builder, not this plan)
web/src/pages/HowItWorks.jsx— static sections per §2,ConceptGif× 4, 2 inline SVGs,anchor ids, back-links to
/,/setup,/explore.web/src/index.jsx— add route;web/src/App.jsx— add nav link.web/test/unit/how-it-works.test.js— source pins (route, nav, anchors, alts, zero-call rule).docs/go/12_web_ui.md— new §2.3.x subsection (Decisions & deviations entry for the page).node --test web/test/unit/*.test.js, real-browser both themes via:9222on thecanonical host,
make fmt && make vet. Commit message names the doc section + decision.Plan review — verdict: proceed-with-fixes (1 blocking)
Reviewed the plan end to end against AGENTS.md (12 laws), docs/go/02 + 05, web/src/index.jsx + App.jsx, docs/go/12 §2.3.0, docs/go/06, plus spot-checks in 03 §7, 08 §8.4, 15 §4.1, features/README, router.go, Landing.jsx. No repo files touched.
Technical-claim verification (sampled highest-risk)
Accurate: manifest fields (head_seq/min_seq/checkpoint/log_segments [min,head]/denormalized packs/settings/revision — 02 §2.2); all 5 entry kinds; checkpoint triggers 256 / 8 MiB / 1 h (05 §5.5+§5.10); group commit 5 ms / 64 (05 §5.10); burn probes 3×100 ms, cap 8 → Corrupt (05 §5.3.2/§5.4); CAS retries 16; new_peeled 16 hops; SETTINGS ≤16 KiB; creationToken = slot epoch (08 §8.4, see nit N1); sync-level table; offline packed-refs rewrite; checkpoint 2-round write + idempotence; min_seq fold; refsAtSeq unreplayable cut; v1 no-native-engine decision; WAL-stays-git-only + P1 prefixes + P9 outs (features/README); framing partial-tail tolerance; "store is the lock" quote. No fabricated claim found.
BLOCKING
B1 — "No server change" contradicts the /explore precedent.
internal/server/router.go:121-129says/exploreMUST be an explicit route, otherwise/*→ repoDispatch treats it as owner "explore" (same shell today, but the?format=textbranch and the name reservation live only on the explicit handler)./how-it-worksis single-segment → identical situation. The plan must: addr.Get("/how-it-works", s.gated(...))next to/explore, addhow-it-worksto the reserved single-segment names (06 §3.3 list + §14 decision entry + router.go comment), and add a server test (GET → 200 shell, no-cache). Without this an owner namedhow-it-workscollides silently.Should-fix
S1 —
?format=textcitation is inverted. The text twin moved FROM/TO/explore(06 §14 decision, #187), not "with the page for Landing". SPA-only v1 recommendation still agreed (text twin lives at/explore;/answers shell unconditionally).S2 — "cold refs = 2" conflates depth with requests. 03 §7: cold-refs requests = 2+tail (no checkpoint: 1+tail); 2 is the depth. Reword as depth-2 to avoid overclaiming.
S3 — claim-drift mitigation: pin config key names, not just doc sections, in the trace comments (
wal.snapshot_every_entries,wal.checkpoint_tail_bytes,wal.checkpoint_interval,wal.batch_window,wal.max_batch,wal.cas_max_retries) — values live in config, sections move.S4 — browser pass must assert
/how-it-worksrenders the HowItWorks hero, not the/:ownerpage (static-vs-param precedence), both themes, plus the already-planned mobile nav-wrap check.Nits
N1 — creationToken = "slot epoch seconds" (08 §8.4). N2 — push budget parenthetical should note "4 if already synced" (03 §7) since the page quotes 5.
Stress-test answers
Route
/how-it-works: good (self-describing;/howrisks collision). Nav 5→6 links: fine, flex-wrap + verify. Zero-API-call via source pins mirroringlanding.test.js: enforceable, agreed. 2 inline SVGs: justified (central CAS claim + positional fold claim, no GIF covers either), keep the 200-line split rule. GIF reuse with verbatim alts: checked againstCONCEPT_ALTS— no staleness, no triple-use, agreed. EVIDENCE no-entry: agreed (page reports sim budgets, makes no hot-path claim). Claim-drift: adequate with S3.Missing for shippable v1 (beyond B1/S4)
Server shell test + reserved-name doc updates (in B1); nothing else — acceptance criteria otherwise cover route/nav/pins/alts/browser/fmt+vet.
Plan revision R1 (review findings — R1 wins on conflict)
Blocking (normative)
/how-it-works(same as/exploreprecedent):r.Get("/how-it-works", s.gated(...))next to/explore; addhow-it-worksto reserved single-segment names (06 §3.3 + §14 decision + router comment); server test (GET → 200 shell, no-cache).Should-fix adoptions
?format=textmoved FROM/TO/explore— SPA-only v1 for the deep-dive, no text twin.wal.snapshot_every_entries,wal.checkpoint_tail_bytes,wal.checkpoint_interval,wal.batch_window,wal.max_batch,wal.cas_max_retries), not just doc sections./how-it-worksrenders the hero (not/:owner), both themes + mobile nav-wrap.Nits adopted
N1 creationToken = slot epoch seconds; N2 push budget "4 if already synced". All other plan text stands.
Implemented as PR #194 (branch feat/issue-191, against main, no conflicts). What shipped: HowItWorks.jsx + HowDiagrams.jsx (2 static SVGs), route + nav + cross-links (Landing, WAL tab), backend explicit howItWorksPage route + reservation docs + server test, how-it-works.test.js, docs 12 §2.3.x + 06 §3.3/§14. R1 B1/S1-S4 + N1/N2 adopted. Verification: gofmt/vet clean; internal/server -race green; make cover gate holds (48 pkgs ok, server 95.6%); node --test 397/397 green; real-Chromium pass on canonical host (hero renders, both themes, 6 anchors + TOC + nav + 4 GIFs + 2 SVGs, zero console errors, no mobile overflow). Deviations: Landing cross-links use #read (not #fetch — matches the section id); diagrams carry role=img (SVG needs its accessible name; the N1 no-role rule is ConceptGif-specific). Not merged — awaiting review.
Review of PR #194 (feat/issue-191, commit
1dd31f5) against issue #191 plan + R1 (plan-review normative). Verified in scratch worktree; main worktree untouched.R1 compliance — all pass
Technical-claim spot-checks (vs 02/05/03/08/15) — no overclaim found
Contract checks — all pass
Tests (scratch worktree, dist concepts staged via public/ copy + node_modules symlink — both scratch-only; main untouched)
Merge recommendation
Ready to merge. No blocking issues, no fixes applied (none needed). Only residual: S4 browser assertion rests on the author's reported Chromium pass — spot-check in post-merge smoke if desired.
Implemented in PR #194 (review: all R1 rulings verified, claims spot-checked, 397/397 + 95.6%), merged. Closing.