JustToThePoint English Website Version
JustToThePoint en español

Building a Verse of the Day Feature. Waterfall Fallback Pattern

Decoupling Linear Systems

For every problem there is always, at least, a solution which seems quite plausible. It is simple and clean, direct, neat and nice, and yet very wrong, #Anawim, justtothepoint.com

There are two ways to do great mathematics. The first is to be smarter than everybody else. The second way is to be stupider than everybody else — but persistent, Raoul Bott

Decoupling Linear Systems

Building a Verse of the Day Feature: Waterfall Fallback Pattern & Testing

This guide walks you through combining offline and online Bible sources to build a getDailyPlanVerse() service, while teaching the waterfall fallback pattern, how to test date‑dependent logic, and why we don’t fire all requests in parallel.

What you will learn

What you will build

One function appended to src/services/bibleService.ts: - getDailyPlanVerse(date?) — the top-level verse-of-the-day function.

And a test that simulates three different dates to verify that the correct references and sources are returned.

Test moment

The Verse tab adds a final panel that calls getDailyPlanVerse() with three simulated dates, printing the reference and source for each.

Waterfall fallback vs parallel race

When you have multiple data sources, you can choose between two strategies:

  1. Parallel race.

    // ❌ Try all sources at once, use whichever resolves first
    const verse = await Promise.race([
      fetchOurManna(),
      fetchYouVersion(key),
      fetchBibleApi(ref),
    ]);
    

    Problem: We launch three network calls simultaneously even if the first one succeeds. This wastes data, battery, and often the user’s bandwidth – especially problematic on mobile.

  2. Waterfall (what we use)

    // ✅ Try sources one at a time, stop at first success
    const verse =
      lookupKJVByRef(ref)     // 1. Offline — instant, always try first
      ?? await fetchBibleApi(ref);  // 2. Online fallback — only if offline fails
    

    The ?? (nullish coalescing) short-circuits: if the left side is non-null or undefined, the right side is never evaluated. fetchBibleApi() is only called if the KJV lookup returned null.
    This matches the Python bible_verse.py cascade:

    # Python waterfall — same logic
    result = _from_daily_plan() or _from_youversion() or _from_ourmanna_api()
    

Append getDailyPlanVerse to src/services/bibleService.ts

We’ll add our new function to the end of src/services/bibleService.ts. It will use these pieces already built in earlier posts:

  1. readLines() from assetReader.ts to load a bundled .txt file
  2. fetchBibleApi() to call the online Bible API
  3. The DailyVerse type (reference, text, source, etc.)
  4. lookupKJVByRef() to search the offline KJV database
// ──────────────────────────────────────────────────────────────────────────────
// DAILY PLAN VERSE SERVICE
// Appended to bibleService.ts
// ──────────────────────────────────────────────────────────────────────────────


// ══════════════════════════════════════════════════════════════════════════════
// PLAN LOADER — reads and caches daily_plan.txt
//
// The plan is a simple text file (data/daily_plan.txt) where each line is a Bible reference like Genesis 1:1.
// We read it once and cache the result so repeated calls to getDailyPlanVerse() don’t re‑read the file.
// ══════════════════════════════════════════════════════════════════════════════

/** Cached lines from daily_plan.txt.
 * `null` means “not yet loaded”.
*/
let _planCache: string[] | null = null;

/**
 * Load and cache the daily reading plan from data/daily_plan.txt.
 *
 * Uses readLines() from assetReader.ts which:
 *   1. Locates the bundled .txt asset via expo-asset
 *   2. Extracts it to a local file URI
 *   3. Reads it as UTF-8 text
 *   4. Strips comment lines (#) and blank lines
 *   5. Returns an array of reference strings
 *
 * @returns Array of reference strings, e.g. ["Genesis 1:1", "John 3:16", ...]
 */
async function loadPlan(): Promise<string[]> {
  if (_planCache !== null) {
    return _planCache;   // already loaded
  }

  try {
    // readLines reads a bundled .txt asset
    // We import it lazily here (inside the function) to avoid a circular
    // import if assetReader ever imports from bibleService.
    const { readLines } = await import("../utils/assetReader");

    // require() works with Expo’s bundler to point to the asset
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    const lines = await readLines(require("../../data/daily_plan.txt"));

    _planCache = lines;
    console.log(`[Bible] Daily plan loaded — ${lines.length} references`);
    return lines;

  } catch (e) {
    console.error("[Bible] Failed to load daily_plan.txt:", e);
    // Return an empty array → getDailyPlanVerse will fall back to the API
    return [];
  }
}

/**
 * Reset the plan cache.
 * Used in tests to force a reload, and by the Settings screen when the user updates the plan file.
 */
export function clearPlanCache(): void {
  _planCache = null;
  console.log("[Bible] Plan cache cleared.");
}

// ══════════════════════════════════════════════════════════════════════════════
// MAIN FUNCTION: getDailyPlanVerse
// ══════════════════════════════════════════════════════════════════════════════

/**
 * Get the verse of the day from the personal reading plan.
 *
 * ALGORITHM:
 *   Load the plan, determine the day index, get the reference at that index, e.g. "John 3:16".
 *   Look up the verse text:
 *        a. Offline KJV (lookupKJVByRef) — instant, no network.
 *        b. bible-api.com (fetchBibleApi) — online fallback.
 *        c. Hardcoded fallback — if everything else fails.
 *
 * WHY offline KJV first:
 *   The offline KJV is always available and instant (~0ms after the first
 *   load).  The online APIs add 200-2000ms latency and fail when offline.
 *   For a prayer app used every morning, reliability matters more than
 *   having the "official" translation from a specific API.
 *
 * @param date  The date to calculate the verse for (defaults to today).
 *              Pass a specific Date to test or simulate a different day.
 * @returns     VerseResult with ref, text, and source — NEVER null.
 *              Falls back to a hardcoded verse if all sources fail.
 *
 * @example
 * // Today's verse
 * const verse = await getDailyPlanVerse();
 *
 * // Simulate a specific date for testing
 * const verse = await getDailyPlanVerse(new Date(2026, 0, 1)); // Jan 1
 */
export async function getDailyPlanVerse(date?: Date): Promise<VerseResult> {
  // ── Step 1: Load the plan ────────────────────────────────────────────────
  // loadPlan() is an async function that reads daily_plan.txt and caches the result.
  // The plan is an array of strings – Bible references.
  const plan = await loadPlan();

  // ── Step 2: Calculate today's index ──────────────────────────────────────
  // Calculate today's index: (dayOfYear - 1) % planLength.
  //
  //   (dayOfYear(date) - 1) % planLength
  //   dayOfYear is 1-based, we subtract 1 to make it 0-based
  //
  // We duplicate the calculation inline so bibleService.ts does not depend
  // on dateUtils.ts at the module level (avoids potential circular imports).
  // This is a pragmatic trade‑off – the logic is simple enough to keep the dependency graph clean.
  const target   = date ?? new Date();
  const jan1     = new Date(target.getFullYear(), 0, 1);
  const doy      = Math.floor((target.getTime() - jan1.getTime()) / 86_400_000) + 1;
  // Difference in milliseconds divided by milliseconds per day.
  // + 1 converts the 0‑based day count into a 1‑based day‑of‑year
  const idx      = plan.length > 0 ? (doy - 1) % plan.length : 0;
  // It wraps around if the plan is shorter than the year (e.g., a 30‑day cycle) and makes the index 0‑based.
  const ref      = plan.length > 0 ? plan[idx] : "John 3:16";
  // It guards against an empty plan.

  console.log(
    `[Bible] Daily plan: day ${doy}, index ${idx}/${plan.length}, ref="${ref}"`
  );

  // ── Step 3: Look up the verse text ────────────────────────────────────────
  //
  // Waterfall: try each source in priority order.
  // The code implements a priority cascade:
  // Offline KJV (synchronous)  →  bible‑api.com (async)  →  Hardcoded fallback
  // Offline is instant, works without internet, and never fails.
  // Each ?? only evaluates its right side if the left side is null.
  //
  // Source A: Offline KJV (synchronous — no await needed)
  // It searches a preloaded JSON object (kjv.json). It returns either a VerseResult-like object or null/undefined.
  const offlineResult = lookupKJVByRef(ref);
  if (offlineResult) {
    // If found, the function immediately returns, enriched with a source label that includes the plan position
    // (Day 5 of 365). This feedback helps the user understand where they are in the plan.
    // Every returned VerseResult carries a source string that tells exactly where the text came from.
    // This is invaluable for debugging, support, and transparency.
    // Users can be shown the source, e.g., “KJV (offline) · Day 47 of 365”.
    return {
      ...offlineResult,
      source: `KJV (offline) · Day ${idx + 1} of ${plan.length}`,
    };
  }

  // The console.warn calls serve as a non‑intrusive log when fallback paths activate.
  console.warn(`[Bible] KJV lookup failed for "${ref}", trying online...`);

  // Source B: bible-api.com (online fallback for the specific reference)
  // Only reached if lookupKJVByRef returned a falsy value.
  const apiText = await fetchBibleApi(ref);
  if (apiText) {
    // If successful, a VerseResult is constructed with the API text and a similar source label.
    return {
      ref,
      text:   apiText,
      source: `bible-api.com · Day ${idx + 1} of ${plan.length}`,
    };
  }

  console.warn(`[Bible] bible-api.com also failed for "${ref}".`);

  // Source C: Hardcoded fallback — always works, always returns something
  // This runs only when both offline and online lookups failed. The most common scenario:
  // offline KJV database wasn’t generated/bundled, and the device has no internet connection.
  // The fallback verse is a well‑known passage (John 3:16), hardcoded to ensure the user always sees something.
  // This eliminates the need for null checks throughout the UI.
  return {
    ref:    "John 3:16",
    text:
      "For God so loved the world, that he gave his only begotten Son, " +
      "that whosoever believeth in him should not perish, but have everlasting life.",
    source: "KJV (hardcoded fallback)",
  };
}


// ══════════════════════════════════════════════════════════════════════════════
// HELPER: get reference for a specific date (without fetching text)
// Used by the Verse tab to show "Today's reference: John 3:16" in the UI
// before the async text lookup completes.
// ══════════════════════════════════════════════════════════════════════════════

/**
 * Return only the plan reference for a given date without fetching the text.
 * Useful in the UI for showing a placeholder ("Loading John 3:16...") before the
 * full verse result arrives.
 *
 * @param date  The date (defaults to today)
 * @returns     Reference string, or null if the plan has not loaded yet
 */
export async function getTodayReference(date?: Date): Promise<string | null> {
  const plan = await loadPlan();
  if (plan.length === 0) return null; // Plan unavailable

  // The day‑of‑year calculation is identical to getDailyPlanVerse, but without the fallback reference.
  // If the plan is empty, it returns null to signal "not ready."
  const target = date ?? new Date();
  const jan1   = new Date(target.getFullYear(), 0, 1);
  const doy    = Math.floor((target.getTime() - jan1.getTime()) / 86_400_000) + 1;
  const idx    = (doy - 1) % plan.length;

  return plan[idx];
}

Step 2 — Add the daily plan test panel to the Verse tab

DailyPlanPanel is a diagnostic/test component or widget designed to verify that the daily reading plan logic (getDailyPlanVerse) works correctly. It renders a small card inside the Verse tab and performs three independent checks:

  1. Fetches the daily plan verse for three different dates in parallel – today, January 1, and July 4 of the current year.
  2. Displays the returned verse reference, a snippet of text, and the source for each lookup. This makes it easy to visually confirm that the plan file is being indexed correctly.
  3. Provides a quick sanity check for developers – different dates should return different references (unless the plan is degenerate).

Add this component to app/(tabs)/verse.tsx after KJVPanel:

// ── DailyPlanPanel: test display ────────────────────────────────
function DailyPlanPanel() {
  /**
   * We test three dates:
   *  - Today
   *  - January 1 (start of year)
   *  - July 4 (mid‑year)
   *
   * This gives us three well‑separated indices into daily_plan.txt,
   * making it easy to confirm that getDailyPlanVerse(date) returns
   * distinct verses for distinct dates.
   */
  const testDates = [
    { label: "Today",   date: new Date() },
    { label: "Jan 1",   date: new Date(new Date().getFullYear(), 0, 1) },
    { label: "Jul 4",   date: new Date(new Date().getFullYear(), 6, 4) },
  ];

  /**
   * Local type describing one row in the UI.
   *
   * result can be:
   *  - "loading": fetch in progress
   *  - VerseResult: successful lookup
   *  - null: defensive fallback (getDailyPlanVerse normally never returns null)
   */
  type PlanResult = {
    label:  string; //  – the human-readable name of the test date (e.g., "Today").
    result: VerseResult | "loading" | null; // one of three possible states:
    // "loading" – the verse is still being fetched; a VerseResult object – the successful result;
    // null – an explicit fallback if the lookup returned null (though getDailyPlanVerse guarantees non‑null,
    // so this branch is for defensive programming).
  };

  /**
   * Initial state: all rows start in "loading" mode.
   * Once the Promise.all completes, the entire array is replaced.
   */
  const [results, setResults] = useState<PlanResult[]>(
    testDates.map(({ label }) => ({ label, result: "loading" }))
  );

  // Data Fetching
  useEffect(() => {
    // Run all three lookups in parallel
    // It’s the simplest way to fetch three independent resources in parallel.
    Promise.all(
      // Maps over the three test dates, creating an async function for each.
      // Promise.all always resolves to an array containing the resolved values of each promise inside it.
      // It waits until all three lookups are complete,
      // then calls setResults with an array of { label, result } objects.
      /* [
        { label: "Today", result: VerseResult },
        { label: "Jan 1", result: VerseResult },
        { label: "Jul 4", result: VerseResult }
      ] */
      testDates.map(async ({ label, date }, i) => {
        // getDailyPlanVerse(date) is called with the specific Date object.
        // This triggers the full waterfall (offline KJV → API → hardcoded) for that date.
        // Each callback is async, so it returns a Promise<{ label, result }>.
        // Promise.all resolves with an array of these values.
        const result = await getDailyPlanVerse(date);
        return { label, result };
      })
    // Promise.all calls setResults with an array of { label, result } objects.
    // That array becomes the argument to .then(...)
    // Because setResults replaces the whole array, the UI re-renders with the final data.
    ).then(setResults);
  // Runs once when the component mounts ([] dependency).
  }, []);

  return (
    <View style={apiStyles.card}>
      <Text style={apiStyles.title}>📅 Daily Plan</Text>
      <Text style={[apiStyles.note, { marginBottom: 12 }]}>
        Each date maps to a different verse from daily_plan.txt.
        Three different dates should give three different references.
      </Text>

      {results.map(({ label, result }) => (
        <View key={label} style={apiStyles.resultBlock}>
          <Text style={apiStyles.resultLabel}>{label}</Text>
          {result === "loading" ? (
            <ActivityIndicator color="#c9a84c" size="small" />
          ) : result === null ? (
            <Text style={apiStyles.resultError}>❌ null</Text>
          ) : (
            <>
              <Text style={apiStyles.resultRef}>{result.ref}</Text>
              <Text style={apiStyles.resultText}>
                {result.text.slice(0, 90)}
                {result.text.length > 90 ? "…" : ""}
              </Text>
              <Text style={apiStyles.resultSource}>{result.source}</Text>
            </>
          )}
        </View>
      ))}
    </View>
  );
}
/* Add the import at the top of `verse.tsx`: */
import { getDailyPlanVerse } from "../../src/services/bibleService";

Add <DailyPlanPanel /> inside the <ScrollView>, after <KJVPanel />.

Step 3 — Run and test

  1. Run npx expo start --clear.
  2. Navigate to the 📖 Verse tab and scroll to the bottom. You should see:
📅 Daily Plan — Chunk 16
Each date maps to a different verse...

Today         John 3:16
              For God so loved the world, that he gave his only...
              KJV (offline) · Day 26 of 63

Jan 1         Genesis 1:1
              In the beginning God created the heaven and the earth.
              KJV (offline) · Day 1 of 63

Jul 4         Psalm 1:1
              Blessed is the man that walketh not in the counsel of the ungodly...
              KJV (offline) · Day 50 of 63
Bitcoin donation

JustToThePoint Copyright © 2011 - 2026 Anawim. ALL RIGHTS RESERVED. Bilingual e-books, articles, and videos to help your child and your entire family succeed, develop a healthy lifestyle, and have a lot of fun. Social Issues, Join us.

This website uses cookies to improve your navigation experience.
By continuing, you are consenting to our use of cookies, in accordance with our Cookies Policy and Website Terms and Conditions of use.