Back to Funclet
SOURCE / PINNED RELEASE

Made of little things.

Funclet

Release
0b497cfc2f66…
Author-recorded commit
1e531b24f490…
License
LICENSE
Author’s source reference
nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-6b7a37cac1d

Archive hash verified: 71d97a89ed8c40c2…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/services.ts
import { cvm, outbox } from '@napplet/sdk';
import { FAVOURITES_KIND, FAVOURITES_LABEL, STATION_KIND, WAVEFUNC_RELAY, WAVEFUNC_SERVER,
  latestBy, parseFavouriteList, parseMusic, parseNowPlaying, parseStation, stationReference, toolPayload } from './domain.js';
import type { SearchMode, Station } from './domain.js';

const queryOptions = { relays: [WAVEFUNC_RELAY], timeoutMs: 8000 };
const cvmOptions = { timeoutMs: 25000, payment: 'deny' as const };

export async function searchStations(query: string) {
  const response = await outbox.query([{ kinds: [STATION_KIND], limit: 48,
    ...(query.trim() ? { search: query.trim() } : {}) }], { ...queryOptions, limit: 200 });
  // Keep the bounded union of relay candidates until after schema validation:
  // unrelated kind-31237 records can otherwise fill the aggregate display cap.
  const stations = response.events.map(({ event }) => parseStation(event)).filter((s): s is Station => s !== null);
  if (response.error && !stations.length) throw new Error(response.error);
  return { stations: latestBy(stations, s => s.address).slice(0, 48), incomplete: Boolean(response.incomplete || response.error) };
}
export async function readFavourites(pubkey: string) {
  const response = await outbox.query([{ kinds: [FAVOURITES_KIND], authors: [pubkey],
    '#l': [FAVOURITES_LABEL], limit: 100 }], { ...queryOptions, authors: [pubkey] });
  const parsed = response.events.map(({ event }) => parseFavouriteList(event)).filter(s => s !== null);
  if (response.error && !parsed.length) throw new Error(response.error);
  return { lists: latestBy(parsed, l => l.identifier).sort((a, b) => a.name.localeCompare(b.name)),
    incomplete: Boolean(response.incomplete || response.error) };
}
export async function readFavouriteStations(addresses: string[]) {
  const references = addresses.map(stationReference).filter(r => r !== null);
  const stations: Station[] = [];
  let incomplete = false;
  let error: string | undefined;
  // Host permits at most four filters per query; preserve each exact author/d pair.
  for (let index = 0; index < references.length; index += 4) {
    const batch = references.slice(index, index + 4);
    const response = await outbox.query(batch.map(r => ({ kinds: [r.kind], authors: [r.author], '#d': [r.identifier], limit: 1 })), queryOptions);
    error ||= response.error;
    incomplete ||= Boolean(response.incomplete || response.error);
    stations.push(...response.events.map(({ event }) => parseStation(event)).filter(s => s !== null));
  }
  const wanted = new Set(addresses);
  const found = latestBy(stations, s => s.address).filter(s => wanted.has(s.address));
  if (error && !found.length) throw new Error(error);
  return { stations: found, incomplete };
}
export async function searchMusic(query: string, mode: Exclude<SearchMode, 'stations'>) {
  return parseMusic(toolPayload(await cvm.callTool(WAVEFUNC_SERVER, `search_${mode}`,
    { query: query.trim(), limit: 20 }, cvmOptions)), mode);
}
export async function nowPlaying(url: string) {
  // No listener telemetry: only the stream URL is needed for metadata extraction.
  return parseNowPlaying(toolPayload(await cvm.callTool(WAVEFUNC_SERVER,
    'extract_stream_metadata', { url }, cvmOptions)));
}
export function closeCvm(): void {
  try { void cvm.close(WAVEFUNC_SERVER).catch(() => {}); } catch { /* Optional host domain. */ }
}