Skip to content
← Public packages

@kentcdodds/sentry-triage

Sentry triage wakes Cole (Grok Bot) per issue: one active wake per repo (lease + queue), loop-safe Discord. Cole may spawn Cursor for isolated repo work.

src/list-storage.ts

98 lines · 3.0 KB · TypeScript
/**
 * Paginate packageStorage.list until the prefix is exhausted.
 *
 * Live packageStorage returns a page object shaped like
 * `{ entries, truncated, nextStartAfter, pageSize }` (default page ≈ 250).
 * A single `list({ prefix })` therefore silently drops later keys — which is
 * how newer `awaiting-seer` issue records fell past the grace window without
 * ever being seen by `./sweep-stale-awaiting` or `./triage-report`.
 */
export async function listStorageEntries(
	storage: {
		list: (opts: Record<string, unknown>) => Promise<unknown>
	},
	opts: { prefix: string; pageSize?: number; maxPages?: number } = {
		prefix: '',
	},
) {
	const prefix = String(opts.prefix ?? '')
	const pageSize = Math.min(
		1000,
		Math.max(1, Math.floor(Number(opts.pageSize) || 256)),
	)
	const maxPages = Math.min(
		200,
		Math.max(1, Math.floor(Number(opts.maxPages) || 40)),
	)
	const all: unknown[] = []
	let startAfter: string | undefined
	for (let page = 0; page < maxPages; page++) {
		const listed = await storage.list({
			prefix,
			limit: pageSize,
			...(startAfter ? { startAfter } : {}),
		})
		const entries = normalizeListEntries(listed)
		all.push(...entries)
		const next = nextStartAfterOf(listed)
		const truncated = isListTruncated(listed)
		if (!truncated || !next || next === startAfter) break
		startAfter = next
	}
	return all
}

export function normalizeListEntries(listed: unknown): unknown[] {
	if (Array.isArray(listed)) return listed
	if (!listed || typeof listed !== 'object') return []
	const obj = listed as Record<string, unknown>
	const candidates = [obj.keys, obj.entries, obj.items]
	for (const candidate of candidates) {
		if (Array.isArray(candidate)) return candidate
	}
	return []
}

export function nextStartAfterOf(listed: unknown): string | null {
	if (!listed || typeof listed !== 'object') return null
	const obj = listed as Record<string, unknown>
	for (const key of ['nextStartAfter', 'cursor', 'continuationToken', 'nextCursor']) {
		const value = obj[key]
		if (typeof value === 'string' && value) return value
	}
	return null
}

export function isListTruncated(listed: unknown): boolean {
	if (!listed || typeof listed !== 'object') return false
	const obj = listed as Record<string, unknown>
	if (obj.truncated === true) return true
	if (obj.list_complete === false) return true
	return false
}

/**
 * Resolve a list entry into a stored record, fetching by key when the page
 * only returned key metadata.
 */
export async function recordFromListEntry(
	storage: { get: (key: string) => Promise<unknown> },
	entry: unknown,
) {
	if (entry && typeof entry === 'object' && 'value' in entry) {
		const value = (entry as { value?: unknown }).value
		if (value != null) return value
	}
	const key =
		typeof entry === 'string'
			? entry
			: entry && typeof entry === 'object'
				? String(
						(entry as { key?: unknown; name?: unknown }).key ??
							(entry as { name?: unknown }).name ??
							'',
					) || null
				: null
	if (!key) return null
	return (await storage.get(key)) ?? null
}