Problem
Six places make the same GRPC call (grpcClient.resources.getResource):
@shm/shared/resource-loader.ts → createResourceLoader — throws on error, no extra work
web/loaders.ts:370 → getResource — returns not-found, no extra work
web/loaders.ts:407 → loadResource — throws on error, adds author metadata etc (see below)
notify/loaders.ts → getResource — returns not-found, no extra work
notify/loaders.ts → loadResource — throws on error, same as web
desktop/entities.ts:154 → loadResource — returns not-found, no extra work
This means that we have duplicate or conflicting logic about processing the GRPC data, across many different locations in the code.
It is not clear what is the responsibility of a "loader". Does it fetch for related data like authors? Does it handle redirects or not?
What web loadResource adds (via loadResourcePayload)
Author metadata (name, avatar for each document.authors)
Breadcrumb navigation (metadata for each parent path)
Support documents (docs referenced via embeds/links in content)
Support queries (results from query blocks in content)
Directory results (children of home doc and current doc)
Home document (for site navigation header)
isLatest flag (compares current version to latest)
Why It's Confusing
Same name loadResource means different things (desktop returns raw, web returns with author metadata)
createResourceLoader throws on error, but desktop's loadResource catches and returns not-found
Web has both getResource AND loadResource making duplicate GRPC calls
No clear naming for abstraction levels
New Pattern: fetch → resolve → load
Note: this is more about "re-organizing" than "refactoring", so it shouldn't be a very dramatic change. But it should hopefully help us understand what functions are responsible for what.
fetchResource (lowest level)
What it does: GRPC call, parse response, return typed result Redirects: Returns {type: 'redirect', redirectTarget} Not found: Returns {type: 'not-found'} Returns: HMResource = document | comment | redirect | not-found
// Desktop, API endpoints, simple lookups
const resource = await fetchResource(id)
if (resource.type === 'document') { /* use resource.document */ }
if (resource.type === 'redirect') { /* caller decides what to do */ }
if (resource.type === 'not-found') { /* caller decides what to do */ }
resolveResource (mid level)
What it does: Fetches and follows redirects until final target Redirects: Follows automatically (recursive) Not found: Throws HMNotFoundError Returns: HMResolvedResource = document | comment (never redirect)
// When you need final target, don't care about redirect chain
const resource = await resolveResource(id)
// resource.type is 'document' or 'comment', never 'redirect'
loadResource (web, highest level)
What it does: Resolves, triggers P2P discovery on not-found, adds:
Author metadata (name, avatar for each author uid)
Breadcrumb navigation (parent path titles)
Support documents (embedded/referenced docs for rendering)
Support queries (query block results)
Directory results (doc children for nav)
Home document + directory (for site header nav)
isLatest flag
Redirects: Follows automatically Not found: Triggers P2P discovery, retries Returns: WebResourcePayload
loadSiteResource (web, wraps loadResource)
What it does: Calls loadResource + adds:
homeMetadata (site name, icon, theme from account root)
originHomeId (site root account id)
origin (request origin URL)
Error fallback (loads home doc even on error so header renders)
Returns: SiteDocumentPayload