Cookbook

Production-ready recipes for common Gummble workflows.

Resilient paginated fetch

async function* iterApps(token: string) {
  let cursor: string | undefined
  while (true) {
    const url = new URL('https://api.gummble.com/api/v1/apps')
    url.searchParams.set('limit', '100')
    if (cursor) url.searchParams.set('cursor', cursor)
    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${token}` },
    })
    if (!res.ok) throw new Error(`HTTP ${res.status}`)
    const { data, pagination } = await res.json()
    for (const app of data) yield app
    if (!pagination.hasMore) return
    cursor = pagination.cursor
  }
}

Bulk download with rate limit awareness

import pLimit from 'p-limit'
const concurrency = pLimit(4)         // stay under the backoff tier

async function downloadAll(screenIds: string[], token: string) {
  await Promise.all(
    screenIds.map(id =>
      concurrency(() => downloadOne(id, token))
    )
  )
}

Visual search from a clipboard image

async function searchFromClipboard(token: string) {
  const items = await navigator.clipboard.read()
  const item = items.find(i => i.types.some(t => t.startsWith('image/')))
  if (!item) throw new Error('No image in clipboard')
  const type = item.types.find(t => t.startsWith('image/'))!
  const blob = await item.getType(type)

  const form = new FormData()
  form.set('image', blob, 'clipboard.png')
  form.set('limit', '12')

  const res = await fetch(
    'https://api.gummble.com/api/v1/search/visual',
    {
      method: 'POST',
      headers: { Authorization: `Bearer ${token}` },
      body: form,
    }
  )
  return res.json()
}

OAuth refresh helper (client-side)

async function refreshIfNeeded(t: {
  access_token: string
  refresh_token: string
  client_id: string
  expires_at: number
}) {
  if (Date.now() < t.expires_at - 60_000) return t.access_token
  const body = new URLSearchParams({
    grant_type: 'refresh_token',
    refresh_token: t.refresh_token,
    client_id: t.client_id,
    resource: 'https://mcp.gummble.com',
  })
  const res = await fetch('https://api.gummble.com/oauth/token', {
    method: 'POST',
    headers: { 'content-type': 'application/x-www-form-urlencoded' },
    body,
  })
  const data = await res.json()
  t.access_token = data.access_token
  t.refresh_token = data.refresh_token
  t.expires_at = Date.now() + data.expires_in * 1000
  return t.access_token
}