createDataSource
import { createDataSource } from "mountly/data";createDataSource() is for widget data that should be shared across islands without each widget inventing its own cache lifecycle. It wraps an async loader with request dedupe, stale-while-revalidate, retry, abort, subscriptions, and a standard snapshot shape.
Basic usage
Section titled “Basic usage”const products = createDataSource<string, Product>({ staleTime: 30_000, retry: 2, load: async ({ key, signal }) => { const res = await fetch(`/api/products/${key}`, { signal }); if (!res.ok) throw new Error(`Failed to load product ${key}`); return res.json(); },});
const data = await products.read("sku_123");const snapshot = products.getSnapshot("sku_123");Snapshot
Section titled “Snapshot”interface DataSourceSnapshot<T> { status: "idle" | "loading" | "success" | "error"; data: T | undefined; error: unknown; loading: boolean; stale: boolean; cacheHit: boolean; updatedAt: number | null;}Widgets can render consistently from status, loading, error, and data rather than each framework inventing a slightly different shape.
Options
Section titled “Options”| Field | Type | Default | Notes |
|---|---|---|---|
load | ({ key, signal }) => Promise<T> | required | Fetcher for one key. Always receives an AbortSignal. |
cacheKey | (key) => string | string or JSON.stringify(key) | Normalizes cache keys. |
ttl | number | null | null | Hard cache expiry in milliseconds. |
staleTime | number | null | null | Marks data as stale after this many milliseconds. |
staleWhileRevalidate | boolean | true | Return stale data immediately while refreshing in the background. |
retry | number | 0 | Retry count after the first failed attempt. |
retryDelay | number | (attempt, error) => number | 0 | Delay between retries. |
cache | DedupCache<string, T> | new cache | Override storage for tests or advanced sharing. |
Subscribe
Section titled “Subscribe”const off = products.subscribe("sku_123", (snapshot) => { render(snapshot);});
await products.read("sku_123");off();Subscribers receive the current snapshot immediately and then every transition: loading, success, error, and invalidation.
Abort and invalidation
Section titled “Abort and invalidation”products.abort("sku_123"); // abort one in-flight keyproducts.abort(); // abort every in-flight key
products.invalidate("sku_123"); // drop one cached valueproducts.clear(); // drop everythingUse abort() when an island is no longer relevant, such as a route transition or a closing modal. Use invalidate() when a mutation changes server state.
With createOnDemandFeature
Section titled “With createOnDemandFeature”const quoteSource = createDataSource<string, Quote>({ staleTime: 60_000, load: ({ key, signal }) => fetch(`/api/quotes/${key}`, { signal }).then((r) => r.json()),});
const quoteFeature = createOnDemandFeature({ moduleId: "quote-card", moduleUrl: "/widgets/quote-card/dist/index.js", loadData: (ctx) => quoteSource.read(String(ctx.quoteId)), getCacheKey: (ctx) => `quote:${ctx.quoteId}`,});createOnDemandFeature still owns feature lifecycle. createDataSource owns data lifecycle when more than one feature or widget needs the same data semantics.