Skip to content

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.

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");
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.

FieldTypeDefaultNotes
load({ key, signal }) => Promise<T>requiredFetcher for one key. Always receives an AbortSignal.
cacheKey(key) => stringstring or JSON.stringify(key)Normalizes cache keys.
ttlnumber | nullnullHard cache expiry in milliseconds.
staleTimenumber | nullnullMarks data as stale after this many milliseconds.
staleWhileRevalidatebooleantrueReturn stale data immediately while refreshing in the background.
retrynumber0Retry count after the first failed attempt.
retryDelaynumber | (attempt, error) => number0Delay between retries.
cacheDedupCache<string, T>new cacheOverride storage for tests or advanced sharing.
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.

products.abort("sku_123"); // abort one in-flight key
products.abort(); // abort every in-flight key
products.invalidate("sku_123"); // drop one cached value
products.clear(); // drop everything

Use 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.

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.