# BaseContext Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Extract a shared `BaseContext` type so hydrators work with both feed and XRPC contexts without manual field spreading or `as any` casts. **Architecture:** Replace `HydrateContext` with `BaseContext` (shared data access interface). `XrpcContext` extends `BaseContext`. Feed hydrate signature changes from `(ctx: HydrateContext)` to `(ctx: BaseContext, items: Row[])`. No deprecation — clean break. **Tech Stack:** TypeScript, hatk framework (`packages/hatk/src/`), template projects (grain, teal) --- ### Task 1: Add BaseContext and buildBaseContext to hydrate.ts **Files:** - Modify: `packages/hatk/src/hydrate.ts` **Step 1: Replace HydrateContext with BaseContext** Replace the `HydrateContext` interface and `buildHydrateContext` function with: ```ts export interface BaseContext { viewer: { did: string; handle?: string } | null db: { query: (sql: string, params?: unknown[]) => Promise } getRecords: (collection: string, uris: string[]) => Promise>> lookup: (collection: string, field: string, values: string[]) => Promise>> count: (collection: string, field: string, values: string[]) => Promise> labels: (uris: string[]) => Promise> blobUrl: ( did: string, ref: unknown, preset?: 'avatar' | 'banner' | 'feed_thumbnail' | 'feed_fullsize', ) => string | undefined } ``` Key differences from `HydrateContext`: - No `items` field (items are now a separate argument) - No generic `` parameter - `viewer` gains optional `handle` field - `db.query` params typed as `unknown[]` not `any[]` Replace `buildHydrateContext`: ```ts /** Build a BaseContext for hydration. */ export function buildBaseContext(viewer: { did: string; handle?: string } | null): BaseContext { return { viewer, db: { query: querySQL }, getRecords: getRecordsMap, lookup: async (collection, field, values) => { if (values.length === 0) return new Map() const unique = [...new Set(values.filter(Boolean))] return lookupByFieldBatch(collection, field, unique) as any }, count: async (collection, field, values) => { if (values.length === 0) return new Map() const unique = [...new Set(values.filter(Boolean))] return countByFieldBatch(collection, field, unique) }, labels: queryLabelsForUris, blobUrl, } } ``` Note: `buildBaseContext` takes only `viewer`, not `items`. The `as any` on `lookupByFieldBatch` return is acceptable — it's an internal implementation detail where the batch function returns the right shape but TypeScript can't prove it. **Step 2: Verify the file compiles** Run: `cd /Users/chadmiller/code/hatk && npx tsc --noEmit -p packages/hatk/tsconfig.json 2>&1 | head -30` Expected: Errors in `feeds.ts` referencing the old `HydrateContext` name — that's expected, we fix it in Task 2. --- ### Task 2: Update feeds.ts to use BaseContext **Files:** - Modify: `packages/hatk/src/feeds.ts` **Step 1: Update imports** Change: ```ts import { resolveRecords, buildHydrateContext } from './hydrate.ts' import type { HydrateContext, Row } from './hydrate.ts' export type { HydrateContext, Row } ``` To: ```ts import { resolveRecords, buildBaseContext } from './hydrate.ts' import type { BaseContext, Row } from './hydrate.ts' export type { BaseContext, Row } ``` **Step 2: Update FeedHandler interface** Change the `hydrate` field type: ```ts // Before hydrate?: (ctx: HydrateContext) => Promise // After hydrate?: (ctx: BaseContext, items: Row[]) => Promise ``` **Step 3: Update FeedOpts type** Change both union members: ```ts type FeedOpts = | { collection: string view?: string label: string generate: FeedGenerate hydrate?: (ctx: BaseContext, items: Row[]) => Promise } | { collection?: never view?: never label: string generate: FeedGenerate hydrate: (ctx: BaseContext, items: Row[]) => Promise } ``` **Step 4: Update executeFeed** Change the hydrate call site from: ```ts const ctx = buildHydrateContext(items, viewer || null) const hydrated = await handler.hydrate(ctx) ``` To: ```ts const ctx = buildBaseContext(viewer || null) const hydrated = await handler.hydrate(ctx, items) ``` **Step 5: Verify** Run: `cd /Users/chadmiller/code/hatk && npx tsc --noEmit -p packages/hatk/tsconfig.json 2>&1 | head -30` Expected: Errors in `cli.ts` referencing old `HydrateContext` — fixed in Task 4. --- ### Task 3: XrpcContext extends BaseContext in xrpc.ts **Files:** - Modify: `packages/hatk/src/xrpc.ts` **Step 1: Import BaseContext** Add to the imports from `./hydrate.ts`: ```ts import { resolveRecords } from './hydrate.ts' import type { BaseContext } from './hydrate.ts' ``` **Step 2: Update XrpcContext interface** Change from a standalone interface to one that extends BaseContext: ```ts export interface XrpcContext< P = Record, Records extends Record = Record, I = unknown, > extends BaseContext { db: { query: (sql: string, params?: unknown[]) => Promise run: (sql: string, ...params: unknown[]) => Promise } params: P input: I cursor?: string limit: number packCursor: (primary: string | number, cid: string) => string unpackCursor: (cursor: string) => { primary: string; cid: string } | null isTakendown: (did: string) => Promise filterTakendownDids: (dids: string[]) => Promise> search: ( collection: K, q: string, opts?: { limit?: number; cursor?: string; fuzzy?: boolean }, ) => Promise<{ records: Row[]; cursor?: string }> resolve: (uris: string[]) => Promise[]> exists: (collection: string, filters: Record) => Promise } ``` Key changes: - `extends BaseContext` — inherits viewer, db.query, getRecords, lookup, count, labels, blobUrl - `db` field overrides BaseContext's `db` to add `.run` method (TypeScript allows this since it's a superset) - Remove fields already in BaseContext: `getRecords`, `lookup`, `count`, `labels`, `blobUrl` — they're inherited - `viewer` is inherited from BaseContext: `{ did: string; handle?: string } | null` - Tighten `any` types to `unknown` where possible **Step 3: Update context construction in initXrpc and registerXrpcHandler** Both `initXrpc` (line ~200) and `registerXrpcHandler` (line ~263) construct `XrpcContext` objects. Remove the fields that are now inherited from BaseContext (they still need to be set since we're constructing an object literal, not using a builder). The construction stays the same shape but now the types are stricter: ```ts const ctx: XrpcContext = { db: { query: querySQL, run: runSQL }, params, input: input || {}, cursor, limit, viewer, packCursor, unpackCursor, isTakendown: isTakendownDid, filterTakendownDids, search: searchRecords, resolve: resolveRecords as any, getRecords: getRecordsMap, lookup: async (collection, field, values) => { if (values.length === 0) return new Map() const unique = [...new Set(values.filter(Boolean))] return lookupByFieldBatch(collection, field, unique) as any }, count: async (collection, field, values) => { if (values.length === 0) return new Map() const unique = [...new Set(values.filter(Boolean))] return countByFieldBatch(collection, field, unique) }, exists: async (collection, filters) => { const conditions = Object.entries(filters).map(([field, value]) => ({ field, value })) const uri = await findUriByFields(collection, conditions) return uri !== null }, labels: queryLabelsForUris, blobUrl, } ``` The object literal is the same — `extends` just means the interface is compatible, not that construction changes. **Step 4: Verify** Run: `cd /Users/chadmiller/code/hatk && npx tsc --noEmit -p packages/hatk/tsconfig.json 2>&1 | head -30` --- ### Task 4: Update opengraph.ts context construction **Files:** - Modify: `packages/hatk/src/opengraph.ts` **Step 1: No structural changes needed** `opengraph.ts` constructs `XrpcContext` objects (line ~135 in `initOpengraph` and line ~222 in `registerOgHandler`). Since `XrpcContext extends BaseContext`, these constructions are already correct — they include all BaseContext fields. The only change: if TypeScript complains about the `any` types that were tightened to `unknown` in `XrpcContext`, update the `db.query` and `labels` types in the construction to match. **Step 2: Verify** Run: `cd /Users/chadmiller/code/hatk && npx tsc --noEmit -p packages/hatk/tsconfig.json 2>&1 | head -30` --- ### Task 5: Update cli.ts codegen **Files:** - Modify: `packages/hatk/src/cli.ts` **Step 1: Update the import line (around line 1527)** Change: ```ts out += `import { defineFeed as _defineFeed, type FeedResult, type FeedContext, type HydrateContext, type Row } from '@hatk/hatk/feeds'\n` ``` To: ```ts out += `import { defineFeed as _defineFeed, type FeedResult, type FeedContext, type BaseContext, type Row } from '@hatk/hatk/feeds'\n` ``` **Step 2: Update the re-export line (around line 1695)** Change: ```ts out += `export type { HydrateContext, Row } from '@hatk/hatk/feeds'\n` ``` To: ```ts out += `export type { BaseContext, Row } from '@hatk/hatk/feeds'\n` ``` **Step 3: Update defineFeed overloads (around lines 1733-1738)** Change: ```ts out += `export function defineFeed(\n` out += ` opts: { collection: K; view?: string; label: string; generate: FeedGenerate; hydrate?: (ctx: HydrateContext) => Promise }\n` out += `): ReturnType\n` out += `export function defineFeed(\n` out += ` opts: { collection?: never; view?: never; label: string; generate: FeedGenerate; hydrate: (ctx: HydrateContext) => Promise }\n` out += `): ReturnType\n` ``` To: ```ts out += `export function defineFeed(\n` out += ` opts: { collection: K; view?: string; label: string; generate: FeedGenerate; hydrate?: (ctx: BaseContext, items: Row[]) => Promise }\n` out += `): ReturnType\n` out += `export function defineFeed(\n` out += ` opts: { collection?: never; view?: never; label: string; generate: FeedGenerate; hydrate: (ctx: BaseContext, items: Row[]) => Promise }\n` out += `): ReturnType\n` ``` Key change: `hydrate` goes from `(ctx: HydrateContext) => ...` to `(ctx: BaseContext, items: Row[]) => ...`. The second overload uses `Row` instead of `HydrateContext`. **Step 4: Verify hatk compiles** Run: `cd /Users/chadmiller/code/hatk && npx tsc --noEmit -p packages/hatk/tsconfig.json 2>&1 | head -30` Expected: Clean compile. All hatk framework changes are done. **Step 5: Bump version and publish** Run: `cd /Users/chadmiller/code/hatk && npm version prerelease --preid alpha -w packages/hatk` Then commit all hatk changes (hydrate.ts, feeds.ts, xrpc.ts, cli.ts, and version bump). --- ### Task 6: Update grain template — _hydrate.ts **Files:** - Modify: `/Users/chadmiller/code/hatk-template-grain/server/feeds/_hydrate.ts` **Step 1: Install updated hatk** Run: `cd /Users/chadmiller/code/hatk-template-grain && npm install` **Step 2: Regenerate types** Run: `cd /Users/chadmiller/code/hatk-template-grain && npx hatk generate` **Step 3: Update hydrateGalleries signature** Change: ```ts import type { HydrateContext, Row } from "$hatk"; export async function hydrateGalleries(ctx: HydrateContext): Promise { const dids = [...new Set(ctx.items.map((item) => item.did).filter(Boolean))]; ``` To: ```ts import type { BaseContext, Row } from "$hatk"; export async function hydrateGalleries(ctx: BaseContext, items: Row[]): Promise { const dids = [...new Set(items.map((item) => item.did).filter(Boolean))]; ``` **Step 4: Replace all `ctx.items` with `items` in the function body** Every reference to `ctx.items` becomes just `items`: - `ctx.items.map(...)` → `items.map(...)` - `ctx.items.length` → `items.length` These occur at: - Line 8: `ctx.items.map((item) => item.did)` → `items.map((item) => item.did)` - Line 9: `ctx.items.map((item) => item.uri)` → `items.map((item) => item.uri)` - Line 61: `return ctx.items.map((item) => {` → `return items.map((item) => {` **Step 5: Verify** Run: `cd /Users/chadmiller/code/hatk-template-grain && npx tsc --noEmit 2>&1 | head -30` Expected: Errors in feed files and searchGalleries.ts — fixed in Tasks 7-8. --- ### Task 7: Update grain template — feed files **Files:** - Modify: `/Users/chadmiller/code/hatk-template-grain/server/feeds/recent.ts` - Modify: `/Users/chadmiller/code/hatk-template-grain/server/feeds/actor.ts` **Step 1: Update recent.ts** Change: ```ts hydrate: hydrateGalleries, ``` To: ```ts hydrate: (ctx, items) => hydrateGalleries(ctx, items), ``` Or more concisely, since the signature now matches directly: ```ts hydrate: hydrateGalleries, ``` This still works because `hydrateGalleries` now has the signature `(ctx: BaseContext, items: Row[]) => Promise`, which matches the `hydrate` field type `(ctx: BaseContext, items: Row[]) => Promise`. So **no change needed** in recent.ts — the function reference still works. **Step 2: Same for actor.ts — no change needed** **Step 3: Verify** Run: `cd /Users/chadmiller/code/hatk-template-grain && npx tsc --noEmit 2>&1 | head -30` --- ### Task 8: Update grain template — searchGalleries.ts **Files:** - Modify: `/Users/chadmiller/code/hatk-template-grain/server/xrpc/searchGalleries.ts` **Step 1: Replace the ugly field spreading** Change: ```ts const galleries = await hydrateGalleries({ items: result.records, viewer: ctx.viewer, db: ctx.db, getRecords: ctx.getRecords, lookup: ctx.lookup, count: ctx.count, labels: ctx.labels, blobUrl: ctx.blobUrl, } as any); ``` To: ```ts const galleries = await hydrateGalleries(ctx, result.records); ``` This works because `XrpcContext extends BaseContext`, so `ctx` is directly assignable to `BaseContext`. **Step 2: Verify** Run: `cd /Users/chadmiller/code/hatk-template-grain && npx tsc --noEmit 2>&1 | head -30` Expected: Clean compile. --- ### Task 9: Update teal template **Files:** - Modify: `/Users/chadmiller/code/hatk-template-teal/server/feeds/_hydrate.ts` **Step 1: Install updated hatk and regenerate types** Run: ```bash cd /Users/chadmiller/code/hatk-template-teal && npm install && npx hatk generate ``` **Step 2: Update hydratePlays signature** Change: ```ts import { views, type HydrateContext, type Play, type Profile } from "$hatk"; export async function hydratePlays(ctx: HydrateContext) { const dids = [...new Set(ctx.items.map((item) => item.did).filter(Boolean))]; ``` To: ```ts import { views, type BaseContext, type Row, type Play, type Profile } from "$hatk"; export async function hydratePlays(ctx: BaseContext, items: Row[]) { const dids = [...new Set(items.map((item) => item.did).filter(Boolean))]; ``` **Step 3: Replace all `ctx.items` with `items`** - Line 6: `ctx.items.map((item) => item.did)` → `items.map((item) => item.did)` - Line 10: `ctx.items.length` → `items.length` - Line 27: `return ctx.items.map((item) => {` → `return items.map((item) => {` **Step 4: Update feed files if needed** Check `recent.ts` and other feed files. The `hydrate: (ctx) => hydratePlays(ctx)` pattern needs to change to `hydrate: (ctx, items) => hydratePlays(ctx, items)` or just `hydrate: hydratePlays`. In teal's `recent.ts`: ```ts // Before hydrate: (ctx) => hydratePlays(ctx), // After hydrate: hydratePlays, ``` Apply the same pattern to all teal feed files that reference `hydratePlays`. **Step 5: Verify** Run: `cd /Users/chadmiller/code/hatk-template-teal && npx tsc --noEmit 2>&1 | head -30` Expected: Clean compile. --- ## Summary of all changes | File | Change | |------|--------| | `hydrate.ts` | `HydrateContext` → `BaseContext` (no items, add handle to viewer), `buildHydrateContext` → `buildBaseContext` (no items param) | | `feeds.ts` | Import/export `BaseContext`, hydrate signature gets `items` param, `executeFeed` passes ctx and items separately | | `xrpc.ts` | `XrpcContext extends BaseContext`, import BaseContext, tighten types | | `opengraph.ts` | No changes needed (constructs XrpcContext which now extends BaseContext) | | `cli.ts` | Codegen: `HydrateContext` → `BaseContext`, `defineFeed` overloads get `(ctx, items)` signature | | grain `_hydrate.ts` | `(ctx: HydrateContext)` → `(ctx: BaseContext, items: Row[])`, `ctx.items` → `items` | | grain feeds | No changes (function reference still works) | | grain `searchGalleries.ts` | Remove 10-line field spread + `as any`, replace with `hydrateGalleries(ctx, result.records)` | | teal `_hydrate.ts` | `(ctx: HydrateContext)` → `(ctx: BaseContext, items: Row[])`, `ctx.items` → `items` | | teal feeds | `(ctx) => hydratePlays(ctx)` → `hydratePlays` |