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

Configure Feed

Select the types of activity you want to include in your feed.

feat: resolve handle-based actor param in feed layer + APNs improvements

Add resolveHandleToDid to db.ts and use it in executeFeed to automatically
resolve handle-based actor params to DIDs before passing to feed handlers.
Also improve APNs HTTP/2 session with connection logging and request timeout handling.

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

+31 -7
+1 -1
packages/hatk/package.json
··· 1 1 { 2 2 "name": "@hatk/hatk", 3 - "version": "0.0.1-alpha.53", 3 + "version": "0.0.1-alpha.54", 4 4 "license": "MIT", 5 5 "bin": { 6 6 "hatk": "dist/cli.js"
+6
packages/hatk/src/database/db.ts
··· 1726 1726 ]) 1727 1727 } 1728 1728 1729 + export async function resolveHandleToDid(handle: string): Promise<string | null> { 1730 + if (handle.startsWith('did:')) return handle 1731 + const rows = await all<{ did: string }>(`SELECT did FROM _repos WHERE handle = $1 LIMIT 1`, [handle]) 1732 + return rows[0]?.did ?? null 1733 + } 1734 + 1729 1735 export async function filterTakendownDids(dids: string[]): Promise<Set<string>> { 1730 1736 if (dids.length === 0) return new Set() 1731 1737 const placeholders = dids.map((_, i) => `$${i + 1}`).join(',')
+7 -1
packages/hatk/src/feeds.ts
··· 1 1 import { resolve } from 'node:path' 2 2 import { readdirSync } from 'node:fs' 3 3 import { log } from './logger.ts' 4 - import { querySQL, packCursor, unpackCursor, isTakendownDid, filterTakendownDids } from './database/db.ts' 4 + import { querySQL, packCursor, unpackCursor, isTakendownDid, filterTakendownDids, resolveHandleToDid } from './database/db.ts' 5 5 import { resolveRecords, buildBaseContext } from './hydrate.ts' 6 6 import type { BaseContext, Row } from './hydrate.ts' 7 7 import type { Checked } from './lex-types.ts' ··· 255 255 ): Promise<{ items?: unknown[]; uris?: string[]; cursor?: string } | null> { 256 256 const handler = feeds.get(name) 257 257 if (!handler) return null 258 + 259 + // Resolve handle-based actor param to DID 260 + if (params.actor && !params.actor.startsWith('did:')) { 261 + const resolved = await resolveHandleToDid(params.actor) 262 + if (resolved) params.actor = resolved 263 + } 258 264 259 265 const result = await handler.generate(params, cursor, limit, viewer || null) 260 266
+17 -5
packages/hatk/src/push.ts
··· 97 97 const host = pushConfig?.apns.production !== false 98 98 ? 'https://api.push.apple.com' 99 99 : 'https://api.sandbox.push.apple.com' 100 - http2Session = connect(host) 101 - http2Session.on('error', () => { 100 + emit('push', 'connecting', { host }) 101 + http2Session = connect(host, { 102 + peerMaxConcurrentStreams: 100, 103 + }) 104 + http2Session.on('connect', () => { 105 + emit('push', 'connected', { host }) 106 + }) 107 + http2Session.on('error', (err: Error) => { 108 + emit('push', 'connection_error', { host, error: err.message }) 102 109 http2Session = null 103 110 }) 104 111 http2Session.on('close', () => { ··· 154 161 155 162 return new Promise<void>((resolve) => { 156 163 const req = session.request(headers) 164 + let settled = false 165 + const done = () => { if (settled) return; settled = true; resolve() } 166 + 157 167 req.setTimeout(15_000, () => { 158 168 req.close() 159 169 emit('push', 'send_error', { did: original.did, error: 'APNs request timed out' }) 160 - resolve() 170 + done() 161 171 }) 162 172 let status = 0 163 173 let body = '' ··· 169 179 body += chunk.toString() 170 180 }) 171 181 req.on('end', async () => { 182 + if (settled) return 172 183 if (status === 200) { 173 184 emit('push', 'sent', { did: original.did, token: token.slice(0, 8) + '...' }) 174 185 } else if (status === 410) { ··· 182 193 body: body.slice(0, 200), 183 194 }) 184 195 } 185 - resolve() 196 + done() 186 197 }) 187 198 req.on('error', (err: Error) => { 199 + if (settled) return 188 200 emit('push', 'send_error', { did: original.did, error: err.message }) 189 - resolve() 201 + done() 190 202 }) 191 203 192 204 req.write(payload)