AppView in a box as a Vite plugin thing hatk.dev

chore: formatting, lint fixes, and minor improvements from prior sessions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+76 -65
+1 -3
docs/site/.vitepress/config.ts
··· 59 59 }, 60 60 ], 61 61 62 - socialLinks: [ 63 - { icon: 'github', link: 'https://github.com/bigmoves/atconf-workshop' }, 64 - ], 62 + socialLinks: [{ icon: 'github', link: 'https://github.com/bigmoves/atconf-workshop' }], 65 63 }, 66 64 })
+3 -3
docs/site/api/index.md
··· 45 45 46 46 ## Built-in endpoints 47 47 48 - | Endpoint | Type | Auth | Description | 49 - | ------------------------------------- | --------- | ---- | ------------------------------- | 48 + | Endpoint | Type | Auth | Description | 49 + | ------------------------------------ | --------- | ---- | ------------------------------- | 50 50 | [`getRecord`](/api/records) | Query | No | Fetch a single record by AT URI | 51 51 | [`getRecords`](/api/records) | Query | No | List records with filters | 52 52 | [`createRecord`](/api/records) | Procedure | Yes | Create a record via user's PDS | ··· 59 59 | [`getPreferences`](/api/preferences) | Query | Yes | Get user preferences | 60 60 | [`putPreference`](/api/preferences) | Procedure | Yes | Set a preference | 61 61 | [`describeLabels`](/api/labels) | Query | No | List label definitions | 62 - | `describeCollections` | Query | No | List indexed collections | 62 + | `describeCollections` | Query | No | List indexed collections |
+15 -15
docs/site/guides/deployment.md
··· 36 36 37 37 ```ts 38 38 export default defineConfig({ 39 - databaseEngine: "sqlite", 40 - database: process.env.NODE_ENV === "production" ? "/data/app.db" : "data/app.db", 41 - }); 39 + databaseEngine: 'sqlite', 40 + database: process.env.NODE_ENV === 'production' ? '/data/app.db' : 'data/app.db', 41 + }) 42 42 ``` 43 43 44 44 ### Health checks ··· 88 88 89 89 ```ts 90 90 // server/setup/create-indexes.ts 91 - import { defineSetup } from "$hatk"; 91 + import { defineSetup } from '$hatk' 92 92 93 93 export default defineSetup(async (ctx) => { 94 - const { db } = ctx; 94 + const { db } = ctx 95 95 await db.run( 96 96 `CREATE INDEX IF NOT EXISTS idx_plays_played_time 97 97 ON "fm.teal.alpha.feed.play"(played_time DESC)`, 98 - ); 99 - }); 98 + ) 99 + }) 100 100 ``` 101 101 102 102 Setup scripts run on every startup. `CREATE INDEX IF NOT EXISTS` makes them idempotent. ··· 124 124 For expensive aggregation queries (trending lists, category counts), use stale-while-revalidate caching: 125 125 126 126 ```ts 127 - let cache: { data: any; expires: number } | null = null; 128 - const TTL = 5 * 60 * 1000; 127 + let cache: { data: any; expires: number } | null = null 128 + const TTL = 5 * 60 * 1000 129 129 130 130 async function refresh(db) { 131 - const rows = await db.query(`...`); 132 - cache = { data: rows, expires: Date.now() + TTL }; 133 - return rows; 131 + const rows = await db.query(`...`) 132 + cache = { data: rows, expires: Date.now() + TTL } 133 + return rows 134 134 } 135 135 136 136 // In handler: 137 137 if (cache) { 138 - if (Date.now() >= cache.expires) refresh(db); // background refresh 139 - return ok(cache.data); // serve stale immediately 138 + if (Date.now() >= cache.expires) refresh(db) // background refresh 139 + return ok(cache.data) // serve stale immediately 140 140 } 141 - return ok(await refresh(db)); // first request waits 141 + return ok(await refresh(db)) // first request waits 142 142 ```
+15 -2
packages/hatk/src/adapter.ts
··· 60 60 } 61 61 62 62 /** Routes handled by hatk — everything else can fall through to a framework handler. */ 63 - export const HATK_ROUTES = ['/xrpc/', '/oauth/', '/oauth-client-metadata.json', '/.well-known/', '/og/', '/admin', '/repos', '/info/', '/_health', '/robots.txt', '/auth/logout', '/__dev/'] 63 + export const HATK_ROUTES = [ 64 + '/xrpc/', 65 + '/oauth/', 66 + '/oauth-client-metadata.json', 67 + '/.well-known/', 68 + '/og/', 69 + '/admin', 70 + '/repos', 71 + '/info/', 72 + '/_health', 73 + '/robots.txt', 74 + '/auth/logout', 75 + '/__dev/', 76 + ] 64 77 65 78 export function isHatkRoute(pathname: string): boolean { 66 - return HATK_ROUTES.some(r => pathname.startsWith(r) || pathname === r) 79 + return HATK_ROUTES.some((r) => pathname.startsWith(r) || pathname === r) 67 80 } 68 81 69 82 /**
+7 -17
packages/hatk/src/database/adapters/sqlite-search.ts
··· 12 12 constructor(private port: DatabasePort) {} 13 13 14 14 async indexExists(shadowTable: string): Promise<boolean> { 15 - const rows = await this.port.query( 16 - `SELECT 1 FROM sqlite_master WHERE type='table' AND name IN ($1, $2)`, 17 - [shadowTable, `${shadowTable}_fts`], 18 - ) 15 + const rows = await this.port.query(`SELECT 1 FROM sqlite_master WHERE type='table' AND name IN ($1, $2)`, [ 16 + shadowTable, 17 + `${shadowTable}_fts`, 18 + ]) 19 19 return rows.length >= 2 20 20 } 21 21 ··· 25 25 26 26 // Create shadow data table from source query 27 27 await this.port.execute(`CREATE TABLE ${shadowTable} AS ${sourceQuery}`, []) 28 - await this.port.execute( 29 - `CREATE UNIQUE INDEX IF NOT EXISTS ${shadowTable}_uri ON ${shadowTable}(uri)`, 30 - [], 31 - ) 28 + await this.port.execute(`CREATE UNIQUE INDEX IF NOT EXISTS ${shadowTable}_uri ON ${shadowTable}(uri)`, []) 32 29 33 30 // Create FTS5 virtual table with external content pointing to shadow table 34 31 const colList = searchColumns.join(', ') ··· 82 79 await this.port.execute(`DELETE FROM ${shadowTable} WHERE uri = $1`, [uri]) 83 80 } 84 81 85 - private async _deleteFromFts( 86 - shadowTable: string, 87 - uri: string, 88 - searchColumns: string[], 89 - ): Promise<void> { 82 + private async _deleteFromFts(shadowTable: string, uri: string, searchColumns: string[]): Promise<void> { 90 83 const colList = searchColumns.join(', ') 91 - const rows = await this.port.query( 92 - `SELECT rowid, uri, ${colList} FROM ${shadowTable} WHERE uri = $1`, 93 - [uri], 94 - ) 84 + const rows = await this.port.query(`SELECT rowid, uri, ${colList} FROM ${shadowTable} WHERE uri = $1`, [uri]) 95 85 if (rows.length === 0) return 96 86 97 87 const old = rows[0] as any
+1 -4
packages/hatk/src/database/fts.ts
··· 213 213 lastRebuiltAt.set(collection, new Date().toISOString()) 214 214 } 215 215 216 - export async function buildFtsRow( 217 - collection: string, 218 - uri: string, 219 - ): Promise<Record<string, string | null> | null> { 216 + export async function buildFtsRow(collection: string, uri: string): Promise<Record<string, string | null> | null> { 220 217 const { searchColNames, sourceQuery } = computeFtsSchema(collection) 221 218 if (searchColNames.length === 0) return null 222 219
+6 -1
packages/hatk/src/database/ports.ts
··· 52 52 buildIndex(shadowTable: string, sourceQuery: string, searchColumns: string[]): Promise<void> 53 53 54 54 /** Incrementally update a single record in the FTS index */ 55 - updateIndex?(shadowTable: string, uri: string, row: Record<string, string | null>, searchColumns: string[]): Promise<void> 55 + updateIndex?( 56 + shadowTable: string, 57 + uri: string, 58 + row: Record<string, string | null>, 59 + searchColumns: string[], 60 + ): Promise<void> 56 61 57 62 /** Remove a single record from the FTS index */ 58 63 deleteFromIndex?(shadowTable: string, uri: string, searchColumns: string[]): Promise<void>
+4 -1
packages/hatk/src/labels.ts
··· 113 113 } 114 114 115 115 /** Register a single label module from a scanned server/ module. */ 116 - export function registerLabelModule(name: string, labelMod: { definition?: LabelDefinition; evaluate?: (ctx: LabelRuleContext) => Promise<string[]> }): void { 116 + export function registerLabelModule( 117 + name: string, 118 + labelMod: { definition?: LabelDefinition; evaluate?: (ctx: LabelRuleContext) => Promise<string[]> }, 119 + ): void { 117 120 if (labelMod.definition) { 118 121 labelDefs.push(labelMod.definition) 119 122 }
+10 -7
packages/hatk/src/oauth/server.ts
··· 325 325 326 326 // --- Server-initiated login (no DPoP required from browser) --- 327 327 328 - export async function serverLogin( 329 - config: OAuthConfig, 330 - handle: string, 331 - ): Promise<string> { 328 + export async function serverLogin(config: OAuthConfig, handle: string): Promise<string> { 332 329 // Resolve handle to DID 333 330 let did = handle 334 331 if (!did.startsWith('did:')) { ··· 345 342 const pdsState = randomToken() 346 343 347 344 // PAR to the PDS 348 - const parEndpoint = 349 - discovery.authServerMetadata.pushed_authorization_request_endpoint || `${pdsAuthServer}/oauth/par` 345 + const parEndpoint = discovery.authServerMetadata.pushed_authorization_request_endpoint || `${pdsAuthServer}/oauth/par` 350 346 const serverDpopProof = await createDpopProof(serverPrivateJwk, serverPublicJwk, 'POST', parEndpoint) 351 347 352 348 const scope = config.scopes?.join(' ') || 'atproto transition:generic' ··· 374 370 if (errBody.error === 'use_dpop_nonce') { 375 371 const nonce = pdsParRes.headers.get('DPoP-Nonce') 376 372 if (nonce) { 377 - const retryProof = await createDpopProof(serverPrivateJwk, serverPublicJwk, 'POST', parEndpoint, undefined, nonce) 373 + const retryProof = await createDpopProof( 374 + serverPrivateJwk, 375 + serverPublicJwk, 376 + 'POST', 377 + parEndpoint, 378 + undefined, 379 + nonce, 380 + ) 378 381 const retryRes = await fetch(parEndpoint, { 379 382 method: 'POST', 380 383 headers: { 'Content-Type': 'application/x-www-form-urlencoded', DPoP: retryProof },
+5 -5
packages/hatk/src/opengraph.ts
··· 56 56 meta?: { title?: string; description?: string } 57 57 } 58 58 59 - export function defineOG( 60 - path: string, 61 - generate: (ctx: OpengraphContext) => Promise<OpengraphResult>, 62 - ) { 59 + export function defineOG(path: string, generate: (ctx: OpengraphContext) => Promise<OpengraphResult>) { 63 60 return { __type: 'og' as const, path, generate } 64 61 } 65 62 ··· 198 195 } 199 196 200 197 /** Register a single OG handler from a scanned server/ module. */ 201 - export function registerOgHandler(ogMod: { path: string; generate: (ctx: OpengraphContext) => Promise<OpengraphResult> }): void { 198 + export function registerOgHandler(ogMod: { 199 + path: string 200 + generate: (ctx: OpengraphContext) => Promise<OpengraphResult> 201 + }): void { 202 202 const { pattern, paramNames } = compilePath(ogMod.path) 203 203 const name = ogMod.path.replace(/^\//, '').replace(/\//g, '-').replace(/:/g, '') 204 204
+1 -5
packages/hatk/src/renderer.ts
··· 44 44 * @param ogMeta - Optional OG meta tags to inject 45 45 * @returns Assembled HTML string, or null if no renderer is registered 46 46 */ 47 - export async function renderPage( 48 - template: string, 49 - request: Request, 50 - ogMeta?: string | null, 51 - ): Promise<string | null> { 47 + export async function renderPage(template: string, request: Request, ogMeta?: string | null): Promise<string | null> { 52 48 if (!renderer) return null 53 49 54 50 const manifest = ssrManifest || { getPreloadTags: () => '' }
+1 -1
packages/hatk/src/response.ts
··· 15 15 headers: { 16 16 'Content-Type': 'application/json', 17 17 'Content-Encoding': 'gzip', 18 - 'Vary': 'Accept-Encoding', 18 + Vary: 'Accept-Encoding', 19 19 ...(status === 200 ? { 'Cache-Control': 'no-store' } : {}), 20 20 }, 21 21 })
+7 -1
packages/hatk/src/server-init.ts
··· 63 63 } 64 64 65 65 log(`[server] Initialized from server/ directory:`) 66 - log(` Feeds: ${listFeeds().map((f) => f.name).join(', ') || 'none'}`) 66 + log( 67 + ` Feeds: ${ 68 + listFeeds() 69 + .map((f) => f.name) 70 + .join(', ') || 'none' 71 + }`, 72 + ) 67 73 log(` XRPC: ${listXrpc().join(', ') || 'none'}`) 68 74 log(` Labels: ${getLabelDefinitions().length} definitions`) 69 75 }