feat: add cached catalog read model#105
Draft
knzeng-e wants to merge 2 commits into
Draft
Conversation
✅ Deploy Preview for muzinga canceled.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Outcome
Dotify Home now loads its production catalog through one cacheable API request
instead of making browser RPC work grow with every artist, release, and royalty
recipient.
Artist SmartRuntimes remain authoritative. The new backend read model is a
performance projection of on-chain state, not a replacement source of truth.
Security-sensitive access decisions still read current runtime policy when a
listener opens a protected track.
Closes #86
Local scope and acceptance record:
docs/backlog/26-cached-catalog-read-model.md.Issue and context
Before this PR, initial browsing performed a fan-out from the browser:
The number of browser RPC calls therefore grew approximately with artists,
tracks, and split recipients. A larger sovereign catalog made the first useful
screen slower, even though browsing does not require a security decision for
every release.
That mixed two different responsibilities:
playback or transactions.
This PR separates those responsibilities. That separation is the central
architectural idea to review.
Architecture and key concepts
Read model
A read model is a query-optimized projection of authoritative data. Here,
SmartRuntimes own catalog truth; the backend stores the shape Home needs so
clients do not reconstruct it repeatedly.
This is a small CQRS-style separation: contract writes and security checks use
the authoritative model, while catalog queries use a projection.
Confirmed checkpoint
The indexer observes the chain head but indexes only:
Each snapshot records the safe block number and hash. If that hash no longer
matches the canonical chain, the checkpoint crossed a reorg and the model
performs a deterministic full reindex.
Event indexing plus reconciliation
Events identify what changed cheaply, but the events do not contain every
catalog field. The indexer therefore:
provider gaps.
This combines low steady-state cost with a deterministic recovery path.
Stale-while-revalidate
Cache-Control: public, max-age=30, stale-while-revalidate=120lets a client oredge cache serve known catalog content while refreshing it in the background.
The ETag is derived from catalog content, not every observed head movement, so
unchanged releases remain cacheable. Fresh state and lag are also exposed in
headers.
How it works
1. Startup and persistence
CatalogReadModel.start()loads the validated snapshot before starting RPCpolling. Snapshot writes use a temporary file and atomic rename, so readers
never observe a partially written JSON document.
The snapshot includes:
2. Incremental synchronization
On each poll:
or directory change;
An event from an unknown runtime triggers reindex rather than advancing past
data the current projection cannot explain.
3. API behavior
The API exposes:
GET /api/catalogGET /api/catalog/artists/:artistAddressGET /api/catalog/releases/:contentHashPagination cursors include the catalog revision. If data changes between pages,
the server returns
STALE_CATALOG_CURSORinstead of silently mixing revisions.Every catalog response describes one of these states:
freshstale-cacherpc-outageindexer-outageempty4. Browser behavior
When
VITE_DOTIFY_API_URLis configured:web/src/services/catalog.tspaints a validated browser-cached response;If-None-Match;useCatalogmaps the API model into the existing UI domain model;CatalogProviderdoes not preflight every protected release on startup;When the API is not configured, local/demo mode preserves direct chain
enumeration. A configured production API never silently falls back to the
browser fan-out.
Design decisions and tradeoffs
Atomic JSON before a database
The repository has no database dependency today. An atomic JSON snapshot gives
the first production read boundary with low operational and migration cost.
Tradeoff: it is deliberately single-writer and unsuitable for horizontal API
replicas.
CATALOG_SNAPSHOT_PATHmust be mounted on durable storage. Sharedtransactional storage is a follow-up before horizontal scaling.
Events are hints; contract reads remain canonical
A pure event-sourced projection would be faster to replay but current events do
not carry descriptions, media refs, full metadata, or splits. Re-reading
changed records avoids inventing event-only truth and lets periodic
reconciliation repair drift.
Bounded-stale browsing, current access
Serving stale catalog metadata during an RPC outage is acceptable for
discovery. Releasing protected content from stale policy is not. The catalog
projection is never accepted as an authorization decision.
Revision-bound offset pagination
Offset pagination is sufficient for the production seed size and simple for
clients. Binding the offset to the content revision prevents cross-revision
page mixtures. Keyset pagination can replace it if catalog scale demands it.
Alternatives not chosen
catalog size.
RPC-dependent on every request.
release data and provider gaps need repair.
operational contract yet; the storage interface isolates that future change.
Security, failure, and operations
startup RPC enumeration.
indexer-outage; it does not erase thein-memory last-known projection.
rpc-outage; key delivery continues to failclosed through its existing path.
cd services/api && npm run catalog:reindex.New server settings are documented in
services/api/.env.exampleanddocs/reference/environment-variables.md:CATALOG_SNAPSHOT_PATHCATALOG_POLL_INTERVAL_MSCATALOG_RECONCILE_INTERVAL_MSCATALOG_STALE_AFTER_MSCATALOG_CONFIRMATIONSReview guide
Suggested order
Scope and invariants
docs/backlog/26-cached-catalog-read-model.md.clearly separated from implementation evidence.
Domain contract
services/api/src/services/catalog/types.ts.smuggling authorization decisions into the projection.
Chain adapter
services/api/src/services/catalog/chain.ts.Consistency engine
services/api/src/services/catalog/readModel.ts, thensnapshotStore.ts.persistence failure.
HTTP contract
services/api/src/routes/catalog.ts.ambiguity, and typed 4xx/5xx responses.
Browser cutover
web/src/services/catalog.ts, then the API branch inweb/src/hooks/useCatalog.ts, thenweb/src/app/providers/CatalogProvider.tsx.bypasses, security-sensitive access reads.
Recovery evidence
readModel.test.ts,catalog.test.ts, and the frontend catalog/statustests.
Operational truth
updates.
not overstated.
Verify carefully
the outage state.
Learning checkpoint
After reviewing, a maintainer should be able to explain:
events;
frontend contracts.
Validation
cd services/api && npm run buildcd services/api && npm test(74 tests)cd web && npm run test:unit(173 tests)cd web && npm run buildcd web && npm run lintnode scripts/backlog-sync.mjs --check --offlinenode scripts/backlog-sync.mjs --check --liveKnown limitations and follow-ups
< 250 ms, cold p75< 800 ms, and useful-content< 800 msstill require deployment traces at the production seed size.horizontally scaling the API.
pagination is needed beyond the initial production seed.
does not add a privileged public reindex endpoint.
This PR remains draft until the public performance evidence is attached. Once
that evidence satisfies #86, marking the PR ready and merging it will close the
ticket.
Metadata
Dotify sprints(Project 5)docs/backlog/26-cached-catalog-read-model.mdknzeng-edotify-backlog,P0,backend,frontend,tests,observability