zephyr-native-cache
v1.6.0
Published
Native caching module for Module Federation Metro bundles
Keywords
Readme
Zephyr Native Cache
Zephyr Cloud | Zephyr Docs | Discord | Twitter | LinkedIn
A React Native native cache layer for Module Federation Metro bundles. It verifies bundle hashes, caches validated bundles on device, and supports background polling for updates.
Installation
# npm
npm install zephyr-native-cache
# yarn
yarn add zephyr-native-cache
# pnpm
pnpm add zephyr-native-cache
# bun
bun add zephyr-native-cacheUsage
Configure the Module Federation runtime plugin in your Metro MF setup:
runtimePlugins: [require.resolve('zephyr-native-cache/runtime-plugin')];Register the cache once at app startup before loading remotes:
import { ZephyrNativeCache } from 'zephyr-native-cache';
ZephyrNativeCache.register({
maxCacheSizeBytes: 50 * 1024 * 1024,
maxAgeMs: 3 * 24 * 60 * 60 * 1000,
enablePolling: true,
pollIntervalMs: 10 * 60 * 1000,
});Configuration
ZephyrNativeCache.register(config) accepts:
bundleDir: custom storage directory for cached bundlesmaxCacheSizeBytes: max cache size before LRU eviction (default20MB)maxAgeMs: stale threshold for cache entries (default7 days)minCacheSizeBytes: minimum cache size to preserve during cleanupenablePolling: start automatic manifest polling (defaulttrue)pollIntervalMs: polling interval in ms (default5 minutes)forceCacheInDev: enable cache in development mode (production is always enabled)
Runtime APIs
ZephyrNativeCache is the recommended app-facing API:
register(config?)getStatus()subscribe(listener)checkForUpdates(options?)startUpdatePolling(intervalMs?)stopUpdatePolling()clearCache()reloadApp()— reloads the React Native JS context after an update is readyrestart()— alias forreloadApp()for compatibility with existing app code
ZephyrNativeCache.register returns a BundleCacheLayer instance:
loadBundle(bundleUrl)checkForUpdates()startPolling(intervalMs?)stopPolling()clearCache()getLoadedBundles()
checkForUpdates also supports policy options:
checkForUpdates({ policy: 'downloadOnly' })checkForUpdates({ policy: 'downloadAndApply' })
downloadAndApply reloads the React Native JS context through the native reload path after a successful download. Avoid using it in flows that cannot tolerate an immediate reload, or persist any critical UI state before calling it.
The package also exposes status helpers as named exports:
getCacheStatus()subscribeCacheStatus(listener)
And package-level control helpers for existing integrations:
checkForUpdates(options?)startUpdatePolling(intervalMs?)stopUpdatePolling()clearCache()
The raw React Native native module is intentionally not exported from the root API. Use ZephyrNativeCache.reloadApp() instead of calling native module methods directly.
It also exposes legacy globals for manual control:
globalThis.__MFE_CHECK_UPDATES__(options?)- e.g.
globalThis.__MFE_CHECK_UPDATES__({ policy: 'downloadOnly' }) - default when omitted:
{ policy: 'downloadOnly' }
- e.g.
globalThis.__MFE_START_UPDATE_POLLING__(intervalMs?)globalThis.__MFE_STOP_UPDATE_POLLING__()
Events
Use CacheEvents to observe cache lifecycle events:
bundle:loadpoll:startupdate:availableupdate:downloadedpoll:complete
Example:
import { ZephyrNativeCache } from 'zephyr-native-cache';
const cache = ZephyrNativeCache.register();
cache.events.on('bundle:load', (event) => {
console.log('[cache]', event.status, event.remoteName);
});
cache.events.on('poll:complete', (event) => {
console.log('[cache] poll complete', event.updated, '/', event.checked);
});React Hook
For React Native UIs, use the built-in hook:
import { useCacheStatus } from 'zephyr-native-cache';
export function CacheStatusPanel() {
const { status, latestUpdateEvent } = useCacheStatus();
return null;
}useCacheStatus exposes runtime state and raw update signals only. UI/notification behavior (toasts, banners, restart prompts, silent apply, etc.) is intentionally app-defined.
Show An Update Prompt
Use latestUpdateEvent as a raw signal to display your own update banner, toast, or modal. The hook does not force any restart UX.
import { Button, Text, View } from 'react-native';
import ZephyrNativeCache, { useCacheStatus } from 'zephyr-native-cache';
export function UpdateBanner() {
const { latestUpdateEvent } = useCacheStatus();
if (!latestUpdateEvent) return null;
return (
<View>
<Text>Update ready for {latestUpdateEvent.remoteName}</Text>
<Button title="Reload" onPress={() => ZephyrNativeCache.reloadApp()} />
</View>
);
}Build Cache Controls
Use the facade for manual update checks and cache invalidation. These calls are safe to wire into dev tools, settings screens, or internal diagnostics.
import { Button, View } from 'react-native';
import ZephyrNativeCache, { useCacheStatus } from 'zephyr-native-cache';
export function CacheControls() {
const { status } = useCacheStatus();
return (
<View>
<Button
title={status.isPolling ? 'Checking...' : 'Check for updates'}
disabled={status.isPolling}
onPress={() => {
ZephyrNativeCache.checkForUpdates({ policy: 'downloadOnly' }).catch((error) =>
console.warn('[cache] Failed to check for updates', error)
);
}}
/>
<Button
title="Clear cache"
onPress={() => {
ZephyrNativeCache.clearCache().catch((error) => {
console.warn('[cache] Failed to clear cache', error);
});
}}
/>
</View>
);
}Render Polling Telemetry
The status snapshot contains enough state to show polling progress, last-check results, and pending remotes.
import { Text, View } from 'react-native';
import { useCacheStatus } from 'zephyr-native-cache';
export function CacheDiagnostics() {
const { status } = useCacheStatus();
const pending = status.pendingUpdates.join(', ') || 'none';
return (
<View>
<Text>Polling: {status.pollingEnabled ? 'enabled' : 'disabled'}</Text>
<Text>Checking now: {status.isPolling ? 'yes' : 'no'}</Text>
<Text>Poll interval: {Math.round(status.pollIntervalMs / 1000)}s</Text>
<Text>Pending updates: {pending}</Text>
{status.lastPollResult && (
<Text>
Last check: {status.lastPollResult.checked} checked, {status.lastPollResult.updated}{' '}
updated
</Text>
)}
</View>
);
}Label Remote Bundle Sources
You can map status.remotes into source badges so operators can see whether each remote came from disk cache or network. This mirrors the pattern used in the native cache test host app.
import { Text } from 'react-native';
import { useCacheStatus, type CacheStatusRemoteEntry } from 'zephyr-native-cache';
const SOURCE_LABELS: Record<string, string> = {
'cache-hit': 'from cache',
downloaded: 'from network',
skipped: 'from network',
pending: 'loading...',
};
function findEntry(
remotes: Record<string, CacheStatusRemoteEntry>,
name: string
): CacheStatusRemoteEntry | undefined {
return (
remotes[name] ??
Object.values(remotes).find(
(entry) => entry.remoteName.endsWith('/' + name) || entry.remoteName === name
)
);
}
export function RemoteSourceBadge({ name }: { name: string }) {
const { status } = useCacheStatus();
const entry = findEntry(status.remotes, name);
const label = entry ? (SOURCE_LABELS[entry.status] ?? entry.status) : 'not loaded';
return (
<Text>
{name}: {label}
</Text>
);
}For a fuller reference, see the zephyr-native-cache-test host app, which combines these patterns into a dev tools panel and source overlay.
Requirements
- React
>=19.0.0 - React Native
>=0.79.0
License
Licensed under the Apache-2.0 License. See LICENSE for more information.
