~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
148 lines · 7.5 KB Raw History

CLAUDE.md

Conventions for working in this repo. These are the ones an agent gets wrong without being told; everything else, read from the code.

Comments

Write a comment only when the information is not obtainable from reading the code. Then keep it concise.

Earns its place:

  • A constraint with no syntax — a construction order one factory depends on, why an unused await is not dead code, why a magic number in an assertion is 3.
  • A domain fact the file cannot show — a single-account server answering RepoNotFound for any other DID.
  • A current bug or quirk an assertion would otherwise look wrong against.

Delete otherwise:

  • Restating the signature, the type, or the next line.
  • Announcing what a well-named function does.
  • Boilerplate repeated across sibling files.
  • Justifying the design decision that produced the code. That belongs in the commit message.
  • Meta-framing: "documents a bug rather than endorsing it", "note that…".

Phrase what stays plainly. Four rules, taken from ASD-STE100 — the parts of it that transfer to code. This is a house style, not a compliance claim; the standard's approved-word list is not applied.

  • One idea per sentence. Twenty words maximum.
  • Active voice. Keep the articles. Not "Only guards this isolate" but "This flag applies to one isolate".
  • No metaphorical verbs. A catch receives an error; it does not swallow it. Code does not sit, live, bail, wrap, punt, or massage.
  • The same word for the same thing, every time. Not handler in one line and endpoint in the next.

The third rule is the one that earns its place. A metaphorical verb is the most common way a comment reads well and means two things.

Describe the present, never the previous arrangement. No "previously X", "used to be Y", "the old 24h cap". In a refactor the tell is usually one small word rather than an explicit "previously":

Don'tDo
"this delegation stays because…""the scheduler is reachable here because…"
"accountApi is still bound off the class""accountApi binds off the class"
"the handlers that remain methods""the handlers that are methods"
"warms the cache the same as before the move""the call warms the DID cache"
"handlers extracted out of pds.js get an entry""each handler module gets an entry"

Each of those only parses if the reader knows the prior arrangement. Commit messages and changesets are exactly where "what changed relative to before" belongs — that distinction is the point.

Types

No any in JSDoc annotations in library or adapter code. Define real typedefs for row and record shapes, or use the core port types (import('@pdsjs/core/ports').SessionRecord). For genuinely dynamic values, type as unknown and cast at the use site: /** @type {SomeType} */ (value).

Test mocks are the exception — casting a mock storage to /** @type {any} */ on the way into a typed constructor is the established pattern across this suite.

Prefer structural types for a handler module's context over indexing off the class (PersonalDataServer['accountApi']). An indexed annotation breaks as soon as the method it names moves.

User-facing copy

No em dashes in anything a user sees — UI strings, banners, notices. Split the clauses into separate sentences instead.

Handler modules

The route handlers live in core/src/handlers/*.js, following the shape @pdsjs/spaces uses: a createXHandlers(ctx) factory taking an explicit context and returning a route table fragment, merged into this.routes in the PersonalDataServer constructor alongside spaceRoutes. The class keeps dispatch, authentication, and the commit and blob machinery.

docs/pds-decomposition.md is the record of how the handlers got there and the mistakes that cost time. Read it before moving code out of pds.js.

  • Name helpers for what they know (crypto, mst, scope), handlers for what they serve (xrpc-sync, account-api). The handlers/ prefix avoids colliding with the same-named helper modules in core/src/.
  • Handler modules stay out of the package export map.
  • Never mix a move with a logic change in one commit. This touches DPoP, client auth and grants; bisectability is the safety net.
  • Before moving a cluster, check inbound references from elsewhere in the same file, not just other packages. handleApiOverview called this.backupsAvailable() from 1,500 lines away and broke three tests.

Account interface

Two route tables describe /account, and they do not match. The React routes are in packages/account-ui/src/app.jsx. The SECTIONS array in core/src/account-ui.js feeds the server-rendered shell, and it names paths the React router does not have. A request for a SECTIONS-only path renders the same page as /account, with no error. Take the routes from app.jsx.

Checks

npm run ci is the gate CI runs: build:ui, biome, tsc --noEmit, then test:coverage. CI enforces it on every push, so run the targeted steps below while iterating and the full gate before committing.

  • pnpm install, not npm install. The workspace dependencies use the workspace:* protocol and npm exits with EUNSUPPORTEDPROTOCOL. CI installs with pnpm install --frozen-lockfile. The npm run scripts work either way.
  • npm run build:ui first on a fresh clone. @pdsjs/account-ui exports a gitignored bundle, and without it the node and cloudflare entrypoints fail to resolve.
  • Two typecheck projects, both run by npm run typecheck. tsconfig.json covers the server packages against Workers and node types. tsconfig.ui.json covers the browser packages' src/lib against the DOM, because the Workers types override it and make a Uint8Array unassignable to BodyInit. types/vite-env.d.ts declares the two import.meta.env values, since vite/client resolves only inside a package that depends on vite.
  • The .jsx files are in neither project. A component's destructured props read as required, so every call site omitting an optional one is an error: 1738 of them, against 136 for the lib modules. Typing the props is the work that would let them in. Until then noUndeclaredVariables is what catches a missing import there, which is the failure that reaches the browser.
  • Coverage floors live in vitest.config.js, per area rather than global. A glob key aggregates every file it matches, so give each module its own entry rather than letting a well-covered one carry a bare one.
  • A minor changeset on a package outside the fixed group in .changeset/config.json escalates the whole group to a major release. The mechanism: @pdsjs/node lists the feature packages in peerDependencies, and changesets major-bumps a dependent when a peer moves by minor. version-packages.js rejects the escalation, but only at release time. Mark such packages patch and give the group its own bump line. A new package's first release needs no changeset at all: set its version directly and changeset publish picks up anything not yet on npm.
  • Changesets may not mix @pdsjs/account-ui with public packages: it is private and unversioned, and changeset version rejects the mix. Leave it out of frontmatter; describe interface work under the packages that embed the built bundle.
  • The e2e suite is not part of the gate; it needs docker-compose. Run it with PLATFORM=node USE_LOCAL_INFRA=false npx vitest run test/e2e.test.js. Three PLC tests fail without the local infra, which is expected. It binds port 2471, so a just dev stack in another workspace will block it.