biblesdk
Version:
Typescript client for Bible SDK API
1 lines • 22.4 kB
Source Map (JSON)
{"version":3,"sources":["../src/http.ts","../src/client.ts"],"names":[],"mappings":"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","sourcesContent":["import { LRUCache } from 'lru-cache'\n\nexport const BASE_URL = 'https://biblesdk.com/api';\n\nconst httpCache = new LRUCache({\n // max number of items in cache\n max: 500,\n // how long items can live in the cache in ms\n ttl: 1000 * 60 * 5,\n // return stale items before removing from cache?\n allowStale: true,\n });\n\nexport async function fancyFetch(\n input: RequestInfo,\n init?: RequestInit,\n timeoutMs = 30000,\n maxRetries = 5,\n baseDelay = 200, // ms\n maxDelay = 30000 // ms\n ): Promise<Response> {\n let delay = baseDelay\n const controller = new AbortController()\n \n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n const attemptTimer = setTimeout(() => controller.abort(), timeoutMs) // timeout per attempt\n try {\n const res = await fetch(input, { ...init, signal: controller.signal })\n \n if (!res.ok && res.status >= 500 && attempt < maxRetries) {\n throw new Error(`HTTP ${res.status}`)\n }\n clearTimeout(attemptTimer) // attempt successful, clear attempt timeout \n return res\n } catch (err) {\n clearTimeout(attemptTimer) // attempt failed, clear attempt timeout \n if (attempt === maxRetries) throw err\n \n delay = Math.min(maxDelay, Math.random() * (delay * 3 - baseDelay) + baseDelay)\n await new Promise((res) => setTimeout(res, delay))\n }\n }\n \n throw new Error(`fetch failed after ${maxRetries} attempts`)\n }\n\nexport async function httpGet<T>(url: string, headers?: Record<string, string>) {\n const cacheKey = `${url}-${JSON.stringify(headers)}`;\n if (httpCache.has(cacheKey)) {\n return httpCache.get(cacheKey) as T;\n }\n const response = await fancyFetch(`${BASE_URL}${url}`, {\n method: 'GET',\n headers: {\n ...headers,\n 'Content-Type': 'application/json',\n },\n });\n const data = response.json() as Promise<T>;\n httpCache.set(cacheKey, data);\n return data;\n}\n","import { httpGet } from \"./http\";\nimport { Book, Chapter, components, ConcordanceInfo, Phrase, PhraseWithConcordanceInfo, Reading, Verse, VerseMatch, VerseRange } from \"./types\";\n\ntype PaginationLinkData = {\n book: string;\n chapter: number;\n take: number;\n cursor: number;\n}\n\nfunction parsePaginationLink(link: string): PaginationLinkData | null {\n const match = link.match(\n /^\\/api\\/books\\/([^/]+)\\/chapters\\/([^/]+)\\/verses(?:\\?([^#]+))?/\n )\n if (match != null) {\n const [_, bookParam, chapterParam, query] = match\n if (bookParam != null && chapterParam != null && query != null) {\n const params = new URLSearchParams(query)\n const takeParam = params.get(\"take\");\n const cursorParam = params.get(\"cursor\");\n if (takeParam != null && cursorParam != null) {\n const take = parseInt(takeParam);\n const cursor = parseInt(cursorParam);\n const chapter = parseInt(chapterParam);\n if (bookParam.length === 3 && chapter > 0 && take > 0 && cursor >= 0) {\n return {\n book: bookParam,\n chapter,\n take,\n cursor,\n }\n }\n }\n }\n }\n return null;\n}\n\nfunction parsePhrases(data: components['schemas']['routes.PhraseDTO'][], book: string, chapter: number | null): Phrase[] {\n const phrases: Phrase[] = [];\n for (const p of data ?? []) {\n if (p.text == null || p.usfm == null || p.position == null) {\n throw new Error(`Failed to parse phrases for book ${book}, chapter ${chapter}, verse ${p.verse}, missing required phrase data`);\n }\n phrases.push({\n book,\n text: p.text,\n usfm: p.usfm,\n position: p.position,\n verse: p.verse ?? null,\n verse_position: p.verse_position ?? null,\n chapter,\n });\n }\n return phrases\n}\n\nfunction parsePhrasesWithConcordanceInfo(data: (components['schemas']['routes.PhraseDTO'] & ConcordanceInfo)[], book: string, chapter: number | null): PhraseWithConcordanceInfo[] {\n const phrases = parsePhrases(data, book, chapter);\n const phrasesWithConcordanceInfo: PhraseWithConcordanceInfo[] = [];\n for (const p of phrases) {\n const phraseIndex = data.findIndex(d => d.position === p.position);\n if (phraseIndex === -1 || data[phraseIndex] == null) {\n throw new Error(`Failed to parse phrases with concordance info for book ${book}, chapter ${chapter}, verse ${p.verse}, missing required phrase data`);\n }\n phrasesWithConcordanceInfo.push({\n ...p,\n strongs_number: data[phraseIndex].strongs_number ?? null,\n strongs_type: data[phraseIndex].strongs_type ?? null,\n transliteration: data[phraseIndex].transliteration ?? null,\n definition: data[phraseIndex].definition ?? null,\n hebrew_word: data[phraseIndex].hebrew_word ?? null,\n greek_word: data[phraseIndex].greek_word ?? null,\n });\n }\n return phrasesWithConcordanceInfo;\n}\n\nasync function parseVerseRange(book: string, chapter: number, verseRange: [number, number]): Promise<VerseRange> {\n const [startVerse, endVerse] = verseRange;\n const chapterMetadata = await getChapterMetadata(book, chapter);\n if (endVerse > chapterMetadata.verses) {\n throw new Error(`Invalid verse range: end verse ${endVerse} out of range for book ${book}, chapter ${chapter}, which has (${chapterMetadata.verses}) verses`);\n }\n if (startVerse < 1 || startVerse > chapterMetadata.verses) {\n throw new Error(`Invalid verse range: start verse ${startVerse} out of range for book ${book}, chapter ${chapter}, which has (${chapterMetadata.verses}) verses`);\n }\n if (startVerse > endVerse) {\n throw new Error(`Invalid verse range: ${startVerse} must be less than or equal to requested end verse ${endVerse} for book ${book}, chapter ${chapter}`);\n }\n return {\n startVerse,\n endVerse,\n };\n}\n\nexport function joinPhrases(phrases: Phrase[]): string {\n const output: string[] = [];\n for (let i = 0; i < phrases.length; i++) {\n const rawPhrase = phrases[i];\n if (rawPhrase != null) {\n output.push(rawPhrase.text.replace(/ {2,}/g, ' '));\n }\n }\n return output.join('');\n}\n\n// list books in the Bible including book_code\nexport async function listBooks(): Promise<Book[]> {\n const data = await httpGet<components['schemas']['routes.BooksListResponse']>('/books');\n if (data.books == null || data.books.length === 0) {\n throw new Error('Failed to list books, no books found');\n }\n const books: Book[] = [];\n for (const b of data.books ?? []) {\n if (b.code == null || b.name == null || b.chapters == null || b.position == null) {\n throw new Error('Failed to list books, missing required book data');\n }\n books.push({\n code: b.code,\n name: b.name,\n chapters: b.chapters,\n position: b.position,\n });\n }\n return books\n}\n\n// list chapters in a book\nexport async function listChapters(book: string): Promise<Chapter[]> {\n const data = await httpGet<components['schemas']['routes.ChaptersListResponse']>(`/books/${book}/chapters`);\n if (data.chapters == null || data.chapters.length === 0) {\n throw new Error(`Failed to list chapters for book ${book}, no chapters found`);\n }\n const chapters: Chapter[] = [];\n for (const c of data.chapters ?? []) {\n if (c.chapter == null || c.position == null || c.verses == null) {\n throw new Error(`Failed to list chapters for book ${book}, missing required chapter data`);\n }\n chapters.push({\n book,\n chapter: c.chapter,\n position: c.position,\n verses: c.verses,\n });\n }\n return chapters\n}\n\n// get info about a book\nexport async function getBookMetadata(book: string): Promise<Book> {\n const data = await httpGet<components['schemas']['routes.BookDTO']>(`/books/${book}`);\n if (data.code == null || data.name == null || data.chapters == null || data.position == null) {\n throw new Error(`Failed to get book metadata for book ${book}, missing required book data`);\n }\n return {\n code: data.code,\n name: data.name,\n chapters: data.chapters,\n position: data.position,\n }\n}\n\n// get info about a chapter\nexport async function getChapterMetadata(book: string, chapter: number): Promise<Chapter> {\n const data = await httpGet<components['schemas']['routes.ChapterDTO']>(`/books/${book}/chapters/${chapter}`);\n if (data.chapter == null || data.position == null || data.verses == null) {\n throw new Error(`Failed to get chapter metadata for book ${book}, chapter ${chapter}, missing required chapter data`);\n }\n return {\n book,\n chapter: data.chapter,\n position: data.position,\n verses: data.verses,\n }\n}\n\n// overload for reading phrases without concordance info\nexport async function read(\n book: string,\n chapter: number,\n cursor?: number,\n take?: number,\n withConcordanceInfo?: false\n): Promise<Reading<Phrase>>;\n\n// overload for reading phrases with concordance info\nexport async function read(\n book: string,\n chapter: number,\n cursor: number,\n take: number,\n withConcordanceInfo: true\n): Promise<Reading<PhraseWithConcordanceInfo>>;\n\n// implementation of overloaded read function\nexport async function read(book: string, chapter: number, cursor: number = 0, take: number = 50, withConcordanceInfo: boolean = false): Promise<Reading<Phrase> | Reading<PhraseWithConcordanceInfo>> {\n // optionally include concordance info\n let url = `/books/${book}/chapters/${chapter}/verses?cursor=${cursor}&take=${take}`;\n if (withConcordanceInfo === true) {\n url += `&concordance=true`;\n }\n\n // start reading at startPhrase\n const data = await httpGet<components['schemas']['routes.PhrasesByChapterResponse']>(url);\n if (data.phrases == null || data.phrases.length === 0) {\n throw new Error(`Failed to read chapter ${chapter} for book ${book}, no phrases found`);\n }\n if (data.links == null) {\n throw new Error(`Error while reading book ${book}, chapter ${chapter}, no next or prev links available`);\n }\n\n // parse phrases from data (optionally including concordance info)\n let phrases: Phrase[] | PhraseWithConcordanceInfo[] = [];\n if (withConcordanceInfo === true) {\n phrases = parsePhrasesWithConcordanceInfo(data.phrases, book, chapter);\n } else {\n phrases = parsePhrases(data.phrases, book, chapter);\n }\n\n // try to extract next or prev input params from provided pagination links \n let n: PaginationLinkData | null = null;\n let p: PaginationLinkData | null = null;\n if (data.links.next != null) {\n n = parsePaginationLink(data.links.next);\n }\n if (data.links.prev != null) {\n p = parsePaginationLink(data.links.prev);\n }\n\n // output provides phrase data with functions to retrieve next or previous sets of phrases\n const output: Reading = {\n phrases,\n next: null,\n prev: null\n }\n if (n != null) {\n output.next = () => read(n.book, n.chapter, n.cursor, n.take);\n }\n if (p != null) {\n output.prev = () => read(p.book, p.chapter, p.cursor, p.take);\n }\n return output;\n}\n\n// overload for getting phrases by verse range without concordance info\nexport async function getPhrases(\n book: string,\n chapter: number,\n verseRange: [number, number],\n withConcordanceInfo?: false\n): Promise<Phrase[]>;\n\n// overload for getting phrases by verse range with concordance info\nexport async function getPhrases(\n book: string,\n chapter: number,\n verseRange: [number, number],\n withConcordanceInfo: true\n): Promise<PhraseWithConcordanceInfo[]>;\n\n// get verses by range as phrases\nexport async function getPhrases(book: string, chapter: number, verseRange: [number, number], withConcordanceInfo: boolean = false): Promise<Phrase[] | PhraseWithConcordanceInfo[]> {\n const { startVerse, endVerse } = await parseVerseRange(book, chapter, verseRange);\n const url = `/books/${book}/chapters/${chapter}/verses/${startVerse}-${endVerse}`;\n if (withConcordanceInfo === true) {\n const data = await httpGet<(components['schemas']['routes.PhraseDTO'] & ConcordanceInfo)[]>(`${url}?concordance=true`);\n return parsePhrasesWithConcordanceInfo(data, book, chapter);\n } else {\n const data = await httpGet<components['schemas']['routes.PhraseDTO'][]>(url);\n return parsePhrases(data, book, chapter,);\n }\n}\n\n// get raw verses by verse range\n// since these are not broken up into phrases, concordance info is unavailable\nexport async function getVerses(book: string, chapter: number, verseRange: [number, number]): Promise<Verse[]> {\n const phrases = await getPhrases(book, chapter, verseRange);\n const verses: Verse[] = [];\n let currentVerse: { number: number, phrases: Phrase[] } = {\n number: 1,\n phrases: []\n };\n for (const p of phrases) {\n if (p.verse == null) {\n continue; // skipping non-verse phrases\n }\n // new verse detected, flush current verse text content\n if (p.verse > currentVerse.number) {\n verses.push({\n book,\n chapter,\n verse: currentVerse.number,\n text: joinPhrases(currentVerse.phrases),\n });\n currentVerse.number = p.verse;\n currentVerse.phrases = [];\n }\n // still accumulating current verse text\n currentVerse.phrases.push(p)\n }\n return verses;\n}\n\n// get search results for a given query\nexport async function getSearchResults(query: string): Promise<VerseMatch[]> {\n const data = await httpGet<components['schemas']['routes.SearchResponse']>(`/search?query=${query}`);\n if (data.matches == null || data.matches.length === 0) {\n throw new Error(`Failed to retrieve search results for query \"${query}\"`);\n }\n const matches: VerseMatch[] = [];\n for (const m of data.matches) {\n if (m.book == null || m.chapter == null || m.verse == null || m.score == null) {\n throw new Error(`Unexpected missing data in search results for query ${query}`);\n }\n matches.push({\n book: m.book,\n chapter: m.chapter,\n verse: m.verse,\n score: m.score,\n });\n }\n return matches;\n}\n"]}