@stackra/sync
v2.0.0
Published
Offline-first synchronization engine — cursor-based pull, batched push, pluggable conflict resolution, and cross-tab coordination.
Maintainers
Readme
@stackra/sync
Offline-first synchronization engine for the Stackra framework — cursor-based pull, batched push with idempotency keys, pluggable conflict resolution, cross-tab coordination, and persistent operation queue.
Install
pnpm add @stackra/sync @stackra/container @stackra/contracts @stackra/http @stackra/network reflect-metadataSubpaths
| Import | Purpose |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| @stackra/sync | SyncModule, SyncEngine, services, resolvers, strategies |
| @stackra/sync/react | WebSyncModule, useSyncStatus, useConflictResolver |
| @stackra/sync/native | NativeSyncModule, AsyncStorageSyncQueue, AppStateChangeListener, ExpoBackgroundFetchTask, native-flavoured useSyncStatus |
| @stackra/sync/testing | Mock client, mock adapters, assertable proxies |
Quick start
import { Module } from "@stackra/container";
import { HttpModule } from "@stackra/http";
import { NetworkModule } from "@stackra/network";
import { CoordinatorModule } from "@stackra/coordinator";
import { SyncModule } from "@stackra/sync";
import { ConflictStrategy } from "@stackra/contracts";
@Module({
imports: [
HttpModule.forRoot({/* ... */}),
NetworkModule.forRoot({/* ... */}),
CoordinatorModule.forRoot({/* ... */}),
SyncModule.forRoot({
baseUrl: "https://api.example.com",
defaultStrategy: ConflictStrategy.LastWriteWins,
autoSyncInterval: 60_000,
autoSyncOnReconnect: true,
batchSize: 50,
}),
],
})
export class AppModule {}Every runtime primitive lives in a separate package:
@stackra/http— transport@stackra/network— online/offline detector@stackra/coordinator— leader-election so only one tab drains the queue@stackra/pipeline— the observable pull/push/full-sync pipelines@stackra/support—BaseRegistry,Str
Every token, interface, enum, and event map is imported directly from
@stackra/contracts — this package re-exports none of them.
React Native usage
The native subpath swaps three moving parts on top of the core module so the same sync engine runs on iOS + Android with native durability semantics:
OPERATION_QUEUE→AsyncStorageSyncQueue. Persists the queue throughIStorageManager.instance('sync-queue')— the storage lives in the@stackra/storage/nativeAsyncStoragedriver. Zero direct AsyncStorage imports outside@stackra/storage/native(perstorage-usage.md§Rule 1).AppStateChangeListener— subscribes to React Native'sAppState.changeevent and emitssync.foreground/sync.backgroundon the sharedEVENT_EMITTERbus. Wired atonApplicationBootstrapby an inline@Injectable()registrar class per ADR-0052.ExpoBackgroundFetchTask(opt-in) — registers a background task withexpo-background-fetch+expo-task-managerthat callsSyncEngine.fullSync()on the OS scheduler. Consumers calltask.register(...)from their own boot hook (background modes require manifest permissions we cannot flip from a library).
import { NativeStorageModule } from "@stackra/storage/native";
import { NativeNetworkModule } from "@stackra/network/native";
import { NativeSyncModule } from "@stackra/sync/native";
import { ConflictStrategy } from "@stackra/contracts";
@Module({
imports: [
NativeStorageModule.forRoot({
default: "preferences",
stores: {
preferences: { driver: "asyncStorage", prefix: "app:prefs" },
// Required for AsyncStorageSyncQueue — the queue's persistence layer.
"sync-queue": { driver: "asyncStorage", prefix: "app:sync" },
},
}),
NativeNetworkModule.forRoot(),
NativeSyncModule.forRoot({
baseUrl: "https://api.example.com",
defaultStrategy: ConflictStrategy.LastWriteWins,
autoSyncInterval: 60_000,
autoSyncOnReconnect: true,
}),
],
})
export class AppModule {}Reactive UI status:
import { useSyncStatus } from "@stackra/sync/native";
function OfflineBanner() {
const { isOnline, pendingCount, isPaused, isSyncing } = useSyncStatus();
if (isOnline && !isPaused && pendingCount === 0) return null;
return (
<View>
<Text>
{isPaused
? "Sync paused (app is backgrounded)"
: !isOnline
? `Working offline — ${pendingCount} changes queued`
: isSyncing
? "Syncing…"
: `${pendingCount} changes queued`}
</Text>
</View>
);
}Opt-in background drain (requires iOS UIBackgroundModes: fetch + Android
background service permissions in the app's manifest):
import { OnApplicationBootstrap, Injectable } from "@stackra/container";
import { ExpoBackgroundFetchTask } from "@stackra/sync/native";
@Injectable()
export class AppBootstrap implements OnApplicationBootstrap {
public constructor(private readonly task: ExpoBackgroundFetchTask) {}
public async onApplicationBootstrap() {
await this.task.register({ minimumIntervalSeconds: 15 * 60 });
}
}Optional peers loaded lazily via dynamic import():
react-native— forAppStateonAppStateChangeListener.expo-background-fetch+expo-task-manager— forExpoBackgroundFetchTask. Missing peers degrade to a fail-soft no-op; the queue + AppState listener still function in-memory.
License
MIT © Figentra L.L.C.
