@tanstack/offline-transactions 1.0.9 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -1
- package/dist/cjs/OfflineExecutor.cjs +5 -5
- package/dist/cjs/OfflineExecutor.cjs.map +1 -1
- package/dist/cjs/OfflineExecutor.d.cts +2 -3
- package/dist/cjs/connectivity/OnlineDetector.cjs +3 -1
- package/dist/cjs/connectivity/OnlineDetector.cjs.map +1 -1
- package/dist/cjs/connectivity/OnlineDetector.d.cts +11 -1
- package/dist/cjs/connectivity/ReactNativeOnlineDetector.cjs +77 -0
- package/dist/cjs/connectivity/ReactNativeOnlineDetector.cjs.map +1 -0
- package/dist/cjs/connectivity/ReactNativeOnlineDetector.d.cts +22 -0
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -1
- package/dist/cjs/outbox/TransactionSerializer.cjs +24 -22
- package/dist/cjs/outbox/TransactionSerializer.cjs.map +1 -1
- package/dist/cjs/react-native/OfflineExecutor.cjs +18 -0
- package/dist/cjs/react-native/OfflineExecutor.cjs.map +1 -0
- package/dist/cjs/react-native/OfflineExecutor.d.cts +14 -0
- package/dist/cjs/react-native/index.cjs +37 -0
- package/dist/cjs/react-native/index.cjs.map +1 -0
- package/dist/cjs/react-native/index.d.cts +3 -0
- package/dist/cjs/types.cjs.map +1 -1
- package/dist/cjs/types.d.cts +9 -1
- package/dist/esm/OfflineExecutor.d.ts +2 -3
- package/dist/esm/OfflineExecutor.js +6 -6
- package/dist/esm/OfflineExecutor.js.map +1 -1
- package/dist/esm/connectivity/OnlineDetector.d.ts +11 -1
- package/dist/esm/connectivity/OnlineDetector.js +4 -2
- package/dist/esm/connectivity/OnlineDetector.js.map +1 -1
- package/dist/esm/connectivity/ReactNativeOnlineDetector.d.ts +22 -0
- package/dist/esm/connectivity/ReactNativeOnlineDetector.js +77 -0
- package/dist/esm/connectivity/ReactNativeOnlineDetector.js.map +1 -0
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/outbox/TransactionSerializer.js +24 -22
- package/dist/esm/outbox/TransactionSerializer.js.map +1 -1
- package/dist/esm/react-native/OfflineExecutor.d.ts +14 -0
- package/dist/esm/react-native/OfflineExecutor.js +18 -0
- package/dist/esm/react-native/OfflineExecutor.js.map +1 -0
- package/dist/esm/react-native/index.d.ts +3 -0
- package/dist/esm/react-native/index.js +37 -0
- package/dist/esm/react-native/index.js.map +1 -0
- package/dist/esm/types.d.ts +9 -1
- package/dist/esm/types.js.map +1 -1
- package/package.json +26 -2
- package/src/OfflineExecutor.ts +5 -4
- package/src/connectivity/OnlineDetector.ts +12 -1
- package/src/connectivity/ReactNativeOnlineDetector.ts +105 -0
- package/src/index.ts +4 -1
- package/src/outbox/TransactionSerializer.ts +27 -27
- package/src/react-native/OfflineExecutor.ts +24 -0
- package/src/react-native/index.ts +45 -0
- package/src/types.ts +9 -2
package/README.md
CHANGED
|
@@ -13,15 +13,46 @@ Offline-first transaction capabilities for TanStack DB that provides durable per
|
|
|
13
13
|
|
|
14
14
|
## Installation
|
|
15
15
|
|
|
16
|
+
### Web
|
|
17
|
+
|
|
16
18
|
```bash
|
|
17
19
|
npm install @tanstack/offline-transactions
|
|
18
20
|
```
|
|
19
21
|
|
|
22
|
+
### React Native / Expo
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @tanstack/offline-transactions @react-native-community/netinfo
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The React Native implementation requires the `@react-native-community/netinfo` peer dependency for network connectivity detection.
|
|
29
|
+
|
|
30
|
+
## Platform Support
|
|
31
|
+
|
|
32
|
+
This package provides platform-specific implementations for web and React Native environments:
|
|
33
|
+
|
|
34
|
+
- **Web**: Uses browser APIs (`window.online/offline` events, `document.visibilitychange`)
|
|
35
|
+
- **React Native**: Uses React Native primitives (`@react-native-community/netinfo` for network status, `AppState` for foreground/background detection)
|
|
36
|
+
|
|
20
37
|
## Quick Start
|
|
21
38
|
|
|
39
|
+
Using offline transactions on web and React Native/Expo is identical except for the import. Choose the appropriate import based on your target platform:
|
|
40
|
+
|
|
41
|
+
**Web:**
|
|
42
|
+
|
|
22
43
|
```typescript
|
|
23
44
|
import { startOfflineExecutor } from '@tanstack/offline-transactions'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**React Native / Expo:**
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { startOfflineExecutor } from '@tanstack/offline-transactions/react-native'
|
|
51
|
+
```
|
|
24
52
|
|
|
53
|
+
**Usage (same for both platforms):**
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
25
56
|
// Setup offline executor
|
|
26
57
|
const offline = startOfflineExecutor({
|
|
27
58
|
collections: { todos: todoCollection },
|
|
@@ -207,13 +238,22 @@ tx.mutate(() => todoCollection.insert({ id: '1', text: 'Buy milk' }))
|
|
|
207
238
|
await tx.commit() // Works offline!
|
|
208
239
|
```
|
|
209
240
|
|
|
210
|
-
##
|
|
241
|
+
## Platform Support
|
|
242
|
+
|
|
243
|
+
### Web Browsers
|
|
211
244
|
|
|
212
245
|
- **IndexedDB**: Modern browsers (primary storage)
|
|
213
246
|
- **localStorage**: Fallback for limited environments
|
|
214
247
|
- **Web Locks API**: Chrome 69+, Firefox 96+ (preferred leader election)
|
|
215
248
|
- **BroadcastChannel**: All modern browsers (fallback leader election)
|
|
216
249
|
|
|
250
|
+
### React Native
|
|
251
|
+
|
|
252
|
+
- **React Native**: 0.60+ (tested with latest versions)
|
|
253
|
+
- **Expo**: SDK 40+ (tested with latest versions)
|
|
254
|
+
- **Required peer dependency**: `@react-native-community/netinfo` for network connectivity detection
|
|
255
|
+
- **Storage**: Uses AsyncStorage or custom storage adapters
|
|
256
|
+
|
|
217
257
|
## License
|
|
218
258
|
|
|
219
259
|
MIT
|
|
@@ -12,7 +12,7 @@ const OnlineDetector = require("./connectivity/OnlineDetector.cjs");
|
|
|
12
12
|
const OfflineTransaction = require("./api/OfflineTransaction.cjs");
|
|
13
13
|
const OfflineAction = require("./api/OfflineAction.cjs");
|
|
14
14
|
const tracer = require("./telemetry/tracer.cjs");
|
|
15
|
-
class OfflineExecutor {
|
|
15
|
+
let OfflineExecutor$1 = class OfflineExecutor {
|
|
16
16
|
constructor(config) {
|
|
17
17
|
this.isLeaderState = false;
|
|
18
18
|
this.unsubscribeOnline = null;
|
|
@@ -20,7 +20,7 @@ class OfflineExecutor {
|
|
|
20
20
|
this.pendingTransactionPromises = /* @__PURE__ */ new Map();
|
|
21
21
|
this.config = config;
|
|
22
22
|
this.scheduler = new KeyScheduler.KeyScheduler();
|
|
23
|
-
this.onlineDetector = new OnlineDetector.
|
|
23
|
+
this.onlineDetector = config.onlineDetector ?? new OnlineDetector.WebOnlineDetector();
|
|
24
24
|
this.storage = null;
|
|
25
25
|
this.outbox = null;
|
|
26
26
|
this.executor = null;
|
|
@@ -365,10 +365,10 @@ class OfflineExecutor {
|
|
|
365
365
|
}
|
|
366
366
|
this.onlineDetector.dispose();
|
|
367
367
|
}
|
|
368
|
-
}
|
|
368
|
+
};
|
|
369
369
|
function startOfflineExecutor(config) {
|
|
370
|
-
return new OfflineExecutor(config);
|
|
370
|
+
return new OfflineExecutor$1(config);
|
|
371
371
|
}
|
|
372
|
-
exports.OfflineExecutor = OfflineExecutor;
|
|
372
|
+
exports.OfflineExecutor = OfflineExecutor$1;
|
|
373
373
|
exports.startOfflineExecutor = startOfflineExecutor;
|
|
374
374
|
//# sourceMappingURL=OfflineExecutor.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OfflineExecutor.cjs","sources":["../../src/OfflineExecutor.ts"],"sourcesContent":["// Storage adapters\nimport { createOptimisticAction, createTransaction } from '@tanstack/db'\nimport { IndexedDBAdapter } from './storage/IndexedDBAdapter'\nimport { LocalStorageAdapter } from './storage/LocalStorageAdapter'\n\n// Core components\nimport { OutboxManager } from './outbox/OutboxManager'\nimport { KeyScheduler } from './executor/KeyScheduler'\nimport { TransactionExecutor } from './executor/TransactionExecutor'\n\n// Coordination\nimport { WebLocksLeader } from './coordination/WebLocksLeader'\nimport { BroadcastChannelLeader } from './coordination/BroadcastChannelLeader'\n\n// Connectivity\nimport { DefaultOnlineDetector } from './connectivity/OnlineDetector'\n\n// API\nimport { OfflineTransaction as OfflineTransactionAPI } from './api/OfflineTransaction'\nimport { createOfflineAction } from './api/OfflineAction'\n\n// TanStack DB primitives\n\n// Replay\nimport { withNestedSpan, withSpan } from './telemetry/tracer'\nimport type {\n CreateOfflineActionOptions,\n CreateOfflineTransactionOptions,\n LeaderElection,\n OfflineConfig,\n OfflineMode,\n OfflineTransaction,\n StorageAdapter,\n StorageDiagnostic,\n} from './types'\nimport type { Transaction } from '@tanstack/db'\n\nexport class OfflineExecutor {\n private config: OfflineConfig\n\n // @ts-expect-error - Set during async initialization in initialize()\n private storage: StorageAdapter | null\n private outbox: OutboxManager | null\n private scheduler: KeyScheduler\n private executor: TransactionExecutor | null\n private leaderElection: LeaderElection | null\n private onlineDetector: DefaultOnlineDetector\n private isLeaderState = false\n private unsubscribeOnline: (() => void) | null = null\n private unsubscribeLeadership: (() => void) | null = null\n\n // Public diagnostic properties\n public readonly mode: OfflineMode\n public readonly storageDiagnostic: StorageDiagnostic\n\n // Track initialization completion\n private initPromise: Promise<void>\n private initResolve!: () => void\n private initReject!: (error: Error) => void\n\n // Coordination mechanism for blocking transactions\n private pendingTransactionPromises: Map<\n string,\n {\n promise: Promise<any>\n resolve: (result: any) => void\n reject: (error: Error) => void\n }\n > = new Map()\n\n constructor(config: OfflineConfig) {\n this.config = config\n this.scheduler = new KeyScheduler()\n this.onlineDetector = new DefaultOnlineDetector()\n\n // Initialize as pending - will be set by async initialization\n this.storage = null\n this.outbox = null\n this.executor = null\n this.leaderElection = null\n\n // Temporary diagnostic - will be updated by async initialization\n this.mode = `offline`\n this.storageDiagnostic = {\n code: `STORAGE_AVAILABLE`,\n mode: `offline`,\n message: `Initializing storage...`,\n }\n\n // Create initialization promise\n this.initPromise = new Promise((resolve, reject) => {\n this.initResolve = resolve\n this.initReject = reject\n })\n\n this.initialize()\n }\n\n /**\n * Probe storage availability and create appropriate adapter.\n * Returns null if no storage is available (online-only mode).\n */\n private async createStorage(): Promise<{\n storage: StorageAdapter | null\n diagnostic: StorageDiagnostic\n }> {\n // If user provided custom storage, use it without probing\n if (this.config.storage) {\n return {\n storage: this.config.storage,\n diagnostic: {\n code: `STORAGE_AVAILABLE`,\n mode: `offline`,\n message: `Using custom storage adapter`,\n },\n }\n }\n\n // Probe IndexedDB first\n const idbProbe = await IndexedDBAdapter.probe()\n if (idbProbe.available) {\n return {\n storage: new IndexedDBAdapter(),\n diagnostic: {\n code: `STORAGE_AVAILABLE`,\n mode: `offline`,\n message: `Using IndexedDB for offline storage`,\n },\n }\n }\n\n // IndexedDB failed, try localStorage\n const lsProbe = LocalStorageAdapter.probe()\n if (lsProbe.available) {\n return {\n storage: new LocalStorageAdapter(),\n diagnostic: {\n code: `INDEXEDDB_UNAVAILABLE`,\n mode: `offline`,\n message: `IndexedDB unavailable, using localStorage fallback`,\n error: idbProbe.error,\n },\n }\n }\n\n // Both failed - determine the diagnostic code\n const isSecurityError =\n idbProbe.error?.name === `SecurityError` ||\n lsProbe.error?.name === `SecurityError`\n const isQuotaError =\n idbProbe.error?.name === `QuotaExceededError` ||\n lsProbe.error?.name === `QuotaExceededError`\n\n let code: StorageDiagnostic[`code`]\n let message: string\n\n if (isSecurityError) {\n code = `STORAGE_BLOCKED`\n message = `Storage blocked (private mode or security restrictions). Running in online-only mode.`\n } else if (isQuotaError) {\n code = `QUOTA_EXCEEDED`\n message = `Storage quota exceeded. Running in online-only mode.`\n } else {\n code = `UNKNOWN_ERROR`\n message = `Storage unavailable due to unknown error. Running in online-only mode.`\n }\n\n return {\n storage: null,\n diagnostic: {\n code,\n mode: `online-only`,\n message,\n error: idbProbe.error || lsProbe.error,\n },\n }\n }\n\n private createLeaderElection(): LeaderElection {\n if (this.config.leaderElection) {\n return this.config.leaderElection\n }\n\n if (WebLocksLeader.isSupported()) {\n return new WebLocksLeader()\n } else if (BroadcastChannelLeader.isSupported()) {\n return new BroadcastChannelLeader()\n } else {\n // Fallback: always be leader in environments without multi-tab support\n return {\n requestLeadership: () => Promise.resolve(true),\n releaseLeadership: () => {},\n isLeader: () => true,\n onLeadershipChange: () => () => {},\n }\n }\n }\n\n private setupEventListeners(): void {\n // Only set up leader election listeners if we have storage\n if (this.leaderElection) {\n this.unsubscribeLeadership = this.leaderElection.onLeadershipChange(\n (isLeader) => {\n this.isLeaderState = isLeader\n\n if (this.config.onLeadershipChange) {\n this.config.onLeadershipChange(isLeader)\n }\n\n if (isLeader) {\n this.loadAndReplayTransactions()\n }\n },\n )\n }\n\n this.unsubscribeOnline = this.onlineDetector.subscribe(() => {\n if (this.isOfflineEnabled && this.executor) {\n // Reset retry delays so transactions can execute immediately when back online\n this.executor.resetRetryDelays()\n this.executor.executeAll().catch((error) => {\n console.warn(\n `Failed to execute transactions on connectivity change:`,\n error,\n )\n })\n }\n })\n }\n\n private async initialize(): Promise<void> {\n return withSpan(`executor.initialize`, {}, async (span) => {\n try {\n // Probe storage and create adapter\n const { storage, diagnostic } = await this.createStorage()\n\n // Cast to writable to set readonly properties\n ;(this as any).storage = storage\n ;(this as any).storageDiagnostic = diagnostic\n ;(this as any).mode = diagnostic.mode\n\n span.setAttribute(`storage.mode`, diagnostic.mode)\n span.setAttribute(`storage.code`, diagnostic.code)\n\n if (!storage) {\n // Online-only mode - notify callback and skip offline setup\n if (this.config.onStorageFailure) {\n this.config.onStorageFailure(diagnostic)\n }\n span.setAttribute(`result`, `online-only`)\n this.initResolve()\n return\n }\n\n // Storage available - set up offline components\n this.outbox = new OutboxManager(storage, this.config.collections)\n this.executor = new TransactionExecutor(\n this.scheduler,\n this.outbox,\n this.config,\n this,\n )\n this.leaderElection = this.createLeaderElection()\n\n // Request leadership first\n const isLeader = await this.leaderElection.requestLeadership()\n this.isLeaderState = isLeader\n span.setAttribute(`isLeader`, isLeader)\n\n // Set up event listeners after leadership is established\n // This prevents the callback from being called multiple times\n this.setupEventListeners()\n\n // Notify initial leadership state\n if (this.config.onLeadershipChange) {\n this.config.onLeadershipChange(isLeader)\n }\n\n if (isLeader) {\n await this.loadAndReplayTransactions()\n }\n span.setAttribute(`result`, `offline-enabled`)\n this.initResolve()\n } catch (error) {\n console.warn(`Failed to initialize offline executor:`, error)\n span.setAttribute(`result`, `failed`)\n this.initReject(\n error instanceof Error ? error : new Error(String(error)),\n )\n }\n })\n }\n\n private async loadAndReplayTransactions(): Promise<void> {\n if (!this.executor) {\n return\n }\n\n try {\n await this.executor.loadPendingTransactions()\n await this.executor.executeAll()\n } catch (error) {\n console.warn(`Failed to load and replay transactions:`, error)\n }\n }\n\n get isOfflineEnabled(): boolean {\n return this.mode === `offline` && this.isLeaderState\n }\n\n createOfflineTransaction(\n options: CreateOfflineTransactionOptions,\n ): Transaction | OfflineTransactionAPI {\n const mutationFn = this.config.mutationFns[options.mutationFnName]\n\n if (!mutationFn) {\n throw new Error(`Unknown mutation function: ${options.mutationFnName}`)\n }\n\n // Check leadership immediately and use the appropriate primitive\n if (!this.isOfflineEnabled) {\n // Non-leader: use createTransaction directly with the resolved mutation function\n // We need to wrap it to add the idempotency key\n return createTransaction({\n autoCommit: options.autoCommit ?? true,\n mutationFn: (params) =>\n mutationFn({\n ...params,\n idempotencyKey: options.idempotencyKey || crypto.randomUUID(),\n }),\n metadata: options.metadata,\n })\n }\n\n // Leader: use OfflineTransaction wrapper for offline persistence\n return new OfflineTransactionAPI(\n options,\n mutationFn,\n this.persistTransaction.bind(this),\n this,\n )\n }\n\n createOfflineAction<T>(options: CreateOfflineActionOptions<T>) {\n const mutationFn = this.config.mutationFns[options.mutationFnName]\n\n if (!mutationFn) {\n throw new Error(`Unknown mutation function: ${options.mutationFnName}`)\n }\n\n // Return a wrapper that checks leadership status at call time\n return (variables: T) => {\n // Check leadership when action is called, not when it's created\n if (!this.isOfflineEnabled) {\n // Non-leader: use createOptimisticAction directly\n const action = createOptimisticAction({\n mutationFn: (vars, params) =>\n mutationFn({\n ...vars,\n ...params,\n idempotencyKey: crypto.randomUUID(),\n }),\n onMutate: options.onMutate,\n })\n return action(variables)\n }\n\n // Leader: use the offline action wrapper\n const action = createOfflineAction(\n options,\n mutationFn,\n this.persistTransaction.bind(this),\n this,\n )\n return action(variables)\n }\n }\n\n private async persistTransaction(\n transaction: OfflineTransaction,\n ): Promise<void> {\n // Wait for initialization to complete\n await this.initPromise\n\n return withNestedSpan(\n `executor.persistTransaction`,\n {\n 'transaction.id': transaction.id,\n 'transaction.mutationFnName': transaction.mutationFnName,\n },\n async (span) => {\n if (!this.isOfflineEnabled || !this.outbox || !this.executor) {\n span.setAttribute(`result`, `skipped_not_leader`)\n this.resolveTransaction(transaction.id, undefined)\n return\n }\n\n try {\n await this.outbox.add(transaction)\n await this.executor.execute(transaction)\n span.setAttribute(`result`, `persisted`)\n } catch (error) {\n console.error(\n `Failed to persist offline transaction ${transaction.id}:`,\n error,\n )\n span.setAttribute(`result`, `failed`)\n throw error\n }\n },\n )\n }\n\n // Method for OfflineTransaction to wait for completion\n async waitForTransactionCompletion(transactionId: string): Promise<any> {\n const existing = this.pendingTransactionPromises.get(transactionId)\n if (existing) {\n return existing.promise\n }\n\n const deferred: {\n promise: Promise<any>\n resolve: (result: any) => void\n reject: (error: Error) => void\n } = {} as any\n\n deferred.promise = new Promise((resolve, reject) => {\n deferred.resolve = resolve\n deferred.reject = reject\n })\n\n this.pendingTransactionPromises.set(transactionId, deferred)\n return deferred.promise\n }\n\n // Method for TransactionExecutor to signal completion\n resolveTransaction(transactionId: string, result: any): void {\n const deferred = this.pendingTransactionPromises.get(transactionId)\n if (deferred) {\n deferred.resolve(result)\n this.pendingTransactionPromises.delete(transactionId)\n }\n }\n\n // Method for TransactionExecutor to signal failure\n rejectTransaction(transactionId: string, error: Error): void {\n const deferred = this.pendingTransactionPromises.get(transactionId)\n if (deferred) {\n deferred.reject(error)\n this.pendingTransactionPromises.delete(transactionId)\n }\n }\n\n async removeFromOutbox(id: string): Promise<void> {\n if (!this.outbox) {\n return\n }\n await this.outbox.remove(id)\n }\n\n async peekOutbox(): Promise<Array<OfflineTransaction>> {\n if (!this.outbox) {\n return []\n }\n return this.outbox.getAll()\n }\n\n async clearOutbox(): Promise<void> {\n if (!this.outbox || !this.executor) {\n return\n }\n await this.outbox.clear()\n this.executor.clear()\n }\n\n notifyOnline(): void {\n this.onlineDetector.notifyOnline()\n }\n\n getPendingCount(): number {\n if (!this.executor) {\n return 0\n }\n return this.executor.getPendingCount()\n }\n\n getRunningCount(): number {\n if (!this.executor) {\n return 0\n }\n return this.executor.getRunningCount()\n }\n\n getOnlineDetector(): DefaultOnlineDetector {\n return this.onlineDetector\n }\n\n dispose(): void {\n if (this.unsubscribeOnline) {\n this.unsubscribeOnline()\n this.unsubscribeOnline = null\n }\n\n if (this.unsubscribeLeadership) {\n this.unsubscribeLeadership()\n this.unsubscribeLeadership = null\n }\n\n if (this.leaderElection) {\n this.leaderElection.releaseLeadership()\n\n if (`dispose` in this.leaderElection) {\n ;(this.leaderElection as any).dispose()\n }\n }\n\n this.onlineDetector.dispose()\n }\n}\n\nexport function startOfflineExecutor(config: OfflineConfig): OfflineExecutor {\n return new OfflineExecutor(config)\n}\n"],"names":["KeyScheduler","DefaultOnlineDetector","IndexedDBAdapter","LocalStorageAdapter","WebLocksLeader","BroadcastChannelLeader","withSpan","OutboxManager","TransactionExecutor","createTransaction","OfflineTransactionAPI","action","createOptimisticAction","createOfflineAction","withNestedSpan"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"OfflineExecutor.cjs","sources":["../../src/OfflineExecutor.ts"],"sourcesContent":["// Storage adapters\nimport { createOptimisticAction, createTransaction } from '@tanstack/db'\nimport { IndexedDBAdapter } from './storage/IndexedDBAdapter'\nimport { LocalStorageAdapter } from './storage/LocalStorageAdapter'\n\n// Core components\nimport { OutboxManager } from './outbox/OutboxManager'\nimport { KeyScheduler } from './executor/KeyScheduler'\nimport { TransactionExecutor } from './executor/TransactionExecutor'\n\n// Coordination\nimport { WebLocksLeader } from './coordination/WebLocksLeader'\nimport { BroadcastChannelLeader } from './coordination/BroadcastChannelLeader'\n\n// Connectivity\nimport { WebOnlineDetector } from './connectivity/OnlineDetector'\n\n// API\nimport { OfflineTransaction as OfflineTransactionAPI } from './api/OfflineTransaction'\nimport { createOfflineAction } from './api/OfflineAction'\n\n// TanStack DB primitives\n\n// Replay\nimport { withNestedSpan, withSpan } from './telemetry/tracer'\nimport type {\n CreateOfflineActionOptions,\n CreateOfflineTransactionOptions,\n LeaderElection,\n OfflineConfig,\n OfflineMode,\n OfflineTransaction,\n OnlineDetector,\n StorageAdapter,\n StorageDiagnostic,\n} from './types'\nimport type { Transaction } from '@tanstack/db'\n\nexport class OfflineExecutor {\n private config: OfflineConfig\n\n // @ts-expect-error - Set during async initialization in initialize()\n private storage: StorageAdapter | null\n private outbox: OutboxManager | null\n private scheduler: KeyScheduler\n private executor: TransactionExecutor | null\n private leaderElection: LeaderElection | null\n private onlineDetector: OnlineDetector\n private isLeaderState = false\n private unsubscribeOnline: (() => void) | null = null\n private unsubscribeLeadership: (() => void) | null = null\n\n // Public diagnostic properties\n public readonly mode: OfflineMode\n public readonly storageDiagnostic: StorageDiagnostic\n\n // Track initialization completion\n private initPromise: Promise<void>\n private initResolve!: () => void\n private initReject!: (error: Error) => void\n\n // Coordination mechanism for blocking transactions\n private pendingTransactionPromises: Map<\n string,\n {\n promise: Promise<any>\n resolve: (result: any) => void\n reject: (error: Error) => void\n }\n > = new Map()\n\n constructor(config: OfflineConfig) {\n this.config = config\n this.scheduler = new KeyScheduler()\n this.onlineDetector = config.onlineDetector ?? new WebOnlineDetector()\n\n // Initialize as pending - will be set by async initialization\n this.storage = null\n this.outbox = null\n this.executor = null\n this.leaderElection = null\n\n // Temporary diagnostic - will be updated by async initialization\n this.mode = `offline`\n this.storageDiagnostic = {\n code: `STORAGE_AVAILABLE`,\n mode: `offline`,\n message: `Initializing storage...`,\n }\n\n // Create initialization promise\n this.initPromise = new Promise((resolve, reject) => {\n this.initResolve = resolve\n this.initReject = reject\n })\n\n this.initialize()\n }\n\n /**\n * Probe storage availability and create appropriate adapter.\n * Returns null if no storage is available (online-only mode).\n */\n private async createStorage(): Promise<{\n storage: StorageAdapter | null\n diagnostic: StorageDiagnostic\n }> {\n // If user provided custom storage, use it without probing\n if (this.config.storage) {\n return {\n storage: this.config.storage,\n diagnostic: {\n code: `STORAGE_AVAILABLE`,\n mode: `offline`,\n message: `Using custom storage adapter`,\n },\n }\n }\n\n // Probe IndexedDB first\n const idbProbe = await IndexedDBAdapter.probe()\n if (idbProbe.available) {\n return {\n storage: new IndexedDBAdapter(),\n diagnostic: {\n code: `STORAGE_AVAILABLE`,\n mode: `offline`,\n message: `Using IndexedDB for offline storage`,\n },\n }\n }\n\n // IndexedDB failed, try localStorage\n const lsProbe = LocalStorageAdapter.probe()\n if (lsProbe.available) {\n return {\n storage: new LocalStorageAdapter(),\n diagnostic: {\n code: `INDEXEDDB_UNAVAILABLE`,\n mode: `offline`,\n message: `IndexedDB unavailable, using localStorage fallback`,\n error: idbProbe.error,\n },\n }\n }\n\n // Both failed - determine the diagnostic code\n const isSecurityError =\n idbProbe.error?.name === `SecurityError` ||\n lsProbe.error?.name === `SecurityError`\n const isQuotaError =\n idbProbe.error?.name === `QuotaExceededError` ||\n lsProbe.error?.name === `QuotaExceededError`\n\n let code: StorageDiagnostic[`code`]\n let message: string\n\n if (isSecurityError) {\n code = `STORAGE_BLOCKED`\n message = `Storage blocked (private mode or security restrictions). Running in online-only mode.`\n } else if (isQuotaError) {\n code = `QUOTA_EXCEEDED`\n message = `Storage quota exceeded. Running in online-only mode.`\n } else {\n code = `UNKNOWN_ERROR`\n message = `Storage unavailable due to unknown error. Running in online-only mode.`\n }\n\n return {\n storage: null,\n diagnostic: {\n code,\n mode: `online-only`,\n message,\n error: idbProbe.error || lsProbe.error,\n },\n }\n }\n\n private createLeaderElection(): LeaderElection {\n if (this.config.leaderElection) {\n return this.config.leaderElection\n }\n\n if (WebLocksLeader.isSupported()) {\n return new WebLocksLeader()\n } else if (BroadcastChannelLeader.isSupported()) {\n return new BroadcastChannelLeader()\n } else {\n // Fallback: always be leader in environments without multi-tab support\n return {\n requestLeadership: () => Promise.resolve(true),\n releaseLeadership: () => {},\n isLeader: () => true,\n onLeadershipChange: () => () => {},\n }\n }\n }\n\n private setupEventListeners(): void {\n // Only set up leader election listeners if we have storage\n if (this.leaderElection) {\n this.unsubscribeLeadership = this.leaderElection.onLeadershipChange(\n (isLeader) => {\n this.isLeaderState = isLeader\n\n if (this.config.onLeadershipChange) {\n this.config.onLeadershipChange(isLeader)\n }\n\n if (isLeader) {\n this.loadAndReplayTransactions()\n }\n },\n )\n }\n\n this.unsubscribeOnline = this.onlineDetector.subscribe(() => {\n if (this.isOfflineEnabled && this.executor) {\n // Reset retry delays so transactions can execute immediately when back online\n this.executor.resetRetryDelays()\n this.executor.executeAll().catch((error) => {\n console.warn(\n `Failed to execute transactions on connectivity change:`,\n error,\n )\n })\n }\n })\n }\n\n private async initialize(): Promise<void> {\n return withSpan(`executor.initialize`, {}, async (span) => {\n try {\n // Probe storage and create adapter\n const { storage, diagnostic } = await this.createStorage()\n\n // Cast to writable to set readonly properties\n ;(this as any).storage = storage\n ;(this as any).storageDiagnostic = diagnostic\n ;(this as any).mode = diagnostic.mode\n\n span.setAttribute(`storage.mode`, diagnostic.mode)\n span.setAttribute(`storage.code`, diagnostic.code)\n\n if (!storage) {\n // Online-only mode - notify callback and skip offline setup\n if (this.config.onStorageFailure) {\n this.config.onStorageFailure(diagnostic)\n }\n span.setAttribute(`result`, `online-only`)\n this.initResolve()\n return\n }\n\n // Storage available - set up offline components\n this.outbox = new OutboxManager(storage, this.config.collections)\n this.executor = new TransactionExecutor(\n this.scheduler,\n this.outbox,\n this.config,\n this,\n )\n this.leaderElection = this.createLeaderElection()\n\n // Request leadership first\n const isLeader = await this.leaderElection.requestLeadership()\n this.isLeaderState = isLeader\n span.setAttribute(`isLeader`, isLeader)\n\n // Set up event listeners after leadership is established\n // This prevents the callback from being called multiple times\n this.setupEventListeners()\n\n // Notify initial leadership state\n if (this.config.onLeadershipChange) {\n this.config.onLeadershipChange(isLeader)\n }\n\n if (isLeader) {\n await this.loadAndReplayTransactions()\n }\n span.setAttribute(`result`, `offline-enabled`)\n this.initResolve()\n } catch (error) {\n console.warn(`Failed to initialize offline executor:`, error)\n span.setAttribute(`result`, `failed`)\n this.initReject(\n error instanceof Error ? error : new Error(String(error)),\n )\n }\n })\n }\n\n private async loadAndReplayTransactions(): Promise<void> {\n if (!this.executor) {\n return\n }\n\n try {\n await this.executor.loadPendingTransactions()\n await this.executor.executeAll()\n } catch (error) {\n console.warn(`Failed to load and replay transactions:`, error)\n }\n }\n\n get isOfflineEnabled(): boolean {\n return this.mode === `offline` && this.isLeaderState\n }\n\n createOfflineTransaction(\n options: CreateOfflineTransactionOptions,\n ): Transaction | OfflineTransactionAPI {\n const mutationFn = this.config.mutationFns[options.mutationFnName]\n\n if (!mutationFn) {\n throw new Error(`Unknown mutation function: ${options.mutationFnName}`)\n }\n\n // Check leadership immediately and use the appropriate primitive\n if (!this.isOfflineEnabled) {\n // Non-leader: use createTransaction directly with the resolved mutation function\n // We need to wrap it to add the idempotency key\n return createTransaction({\n autoCommit: options.autoCommit ?? true,\n mutationFn: (params) =>\n mutationFn({\n ...params,\n idempotencyKey: options.idempotencyKey || crypto.randomUUID(),\n }),\n metadata: options.metadata,\n })\n }\n\n // Leader: use OfflineTransaction wrapper for offline persistence\n return new OfflineTransactionAPI(\n options,\n mutationFn,\n this.persistTransaction.bind(this),\n this,\n )\n }\n\n createOfflineAction<T>(options: CreateOfflineActionOptions<T>) {\n const mutationFn = this.config.mutationFns[options.mutationFnName]\n\n if (!mutationFn) {\n throw new Error(`Unknown mutation function: ${options.mutationFnName}`)\n }\n\n // Return a wrapper that checks leadership status at call time\n return (variables: T) => {\n // Check leadership when action is called, not when it's created\n if (!this.isOfflineEnabled) {\n // Non-leader: use createOptimisticAction directly\n const action = createOptimisticAction({\n mutationFn: (vars, params) =>\n mutationFn({\n ...vars,\n ...params,\n idempotencyKey: crypto.randomUUID(),\n }),\n onMutate: options.onMutate,\n })\n return action(variables)\n }\n\n // Leader: use the offline action wrapper\n const action = createOfflineAction(\n options,\n mutationFn,\n this.persistTransaction.bind(this),\n this,\n )\n return action(variables)\n }\n }\n\n private async persistTransaction(\n transaction: OfflineTransaction,\n ): Promise<void> {\n // Wait for initialization to complete\n await this.initPromise\n\n return withNestedSpan(\n `executor.persistTransaction`,\n {\n 'transaction.id': transaction.id,\n 'transaction.mutationFnName': transaction.mutationFnName,\n },\n async (span) => {\n if (!this.isOfflineEnabled || !this.outbox || !this.executor) {\n span.setAttribute(`result`, `skipped_not_leader`)\n this.resolveTransaction(transaction.id, undefined)\n return\n }\n\n try {\n await this.outbox.add(transaction)\n await this.executor.execute(transaction)\n span.setAttribute(`result`, `persisted`)\n } catch (error) {\n console.error(\n `Failed to persist offline transaction ${transaction.id}:`,\n error,\n )\n span.setAttribute(`result`, `failed`)\n throw error\n }\n },\n )\n }\n\n // Method for OfflineTransaction to wait for completion\n async waitForTransactionCompletion(transactionId: string): Promise<any> {\n const existing = this.pendingTransactionPromises.get(transactionId)\n if (existing) {\n return existing.promise\n }\n\n const deferred: {\n promise: Promise<any>\n resolve: (result: any) => void\n reject: (error: Error) => void\n } = {} as any\n\n deferred.promise = new Promise((resolve, reject) => {\n deferred.resolve = resolve\n deferred.reject = reject\n })\n\n this.pendingTransactionPromises.set(transactionId, deferred)\n return deferred.promise\n }\n\n // Method for TransactionExecutor to signal completion\n resolveTransaction(transactionId: string, result: any): void {\n const deferred = this.pendingTransactionPromises.get(transactionId)\n if (deferred) {\n deferred.resolve(result)\n this.pendingTransactionPromises.delete(transactionId)\n }\n }\n\n // Method for TransactionExecutor to signal failure\n rejectTransaction(transactionId: string, error: Error): void {\n const deferred = this.pendingTransactionPromises.get(transactionId)\n if (deferred) {\n deferred.reject(error)\n this.pendingTransactionPromises.delete(transactionId)\n }\n }\n\n async removeFromOutbox(id: string): Promise<void> {\n if (!this.outbox) {\n return\n }\n await this.outbox.remove(id)\n }\n\n async peekOutbox(): Promise<Array<OfflineTransaction>> {\n if (!this.outbox) {\n return []\n }\n return this.outbox.getAll()\n }\n\n async clearOutbox(): Promise<void> {\n if (!this.outbox || !this.executor) {\n return\n }\n await this.outbox.clear()\n this.executor.clear()\n }\n\n notifyOnline(): void {\n this.onlineDetector.notifyOnline()\n }\n\n getPendingCount(): number {\n if (!this.executor) {\n return 0\n }\n return this.executor.getPendingCount()\n }\n\n getRunningCount(): number {\n if (!this.executor) {\n return 0\n }\n return this.executor.getRunningCount()\n }\n\n getOnlineDetector(): OnlineDetector {\n return this.onlineDetector\n }\n\n dispose(): void {\n if (this.unsubscribeOnline) {\n this.unsubscribeOnline()\n this.unsubscribeOnline = null\n }\n\n if (this.unsubscribeLeadership) {\n this.unsubscribeLeadership()\n this.unsubscribeLeadership = null\n }\n\n if (this.leaderElection) {\n this.leaderElection.releaseLeadership()\n\n if (`dispose` in this.leaderElection) {\n ;(this.leaderElection as any).dispose()\n }\n }\n\n this.onlineDetector.dispose()\n }\n}\n\nexport function startOfflineExecutor(config: OfflineConfig): OfflineExecutor {\n return new OfflineExecutor(config)\n}\n"],"names":["KeyScheduler","WebOnlineDetector","IndexedDBAdapter","LocalStorageAdapter","WebLocksLeader","BroadcastChannelLeader","withSpan","OutboxManager","TransactionExecutor","createTransaction","OfflineTransactionAPI","action","createOptimisticAction","createOfflineAction","withNestedSpan","OfflineExecutor"],"mappings":"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"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { DefaultOnlineDetector } from './connectivity/OnlineDetector.cjs';
|
|
2
1
|
import { OfflineTransaction as OfflineTransactionAPI } from './api/OfflineTransaction.cjs';
|
|
3
|
-
import { CreateOfflineActionOptions, CreateOfflineTransactionOptions, OfflineConfig, OfflineMode, OfflineTransaction, StorageDiagnostic } from './types.cjs';
|
|
2
|
+
import { CreateOfflineActionOptions, CreateOfflineTransactionOptions, OfflineConfig, OfflineMode, OfflineTransaction, OnlineDetector, StorageDiagnostic } from './types.cjs';
|
|
4
3
|
import { Transaction } from '@tanstack/db';
|
|
5
4
|
export declare class OfflineExecutor {
|
|
6
5
|
private config;
|
|
@@ -42,7 +41,7 @@ export declare class OfflineExecutor {
|
|
|
42
41
|
notifyOnline(): void;
|
|
43
42
|
getPendingCount(): number;
|
|
44
43
|
getRunningCount(): number;
|
|
45
|
-
getOnlineDetector():
|
|
44
|
+
getOnlineDetector(): OnlineDetector;
|
|
46
45
|
dispose(): void;
|
|
47
46
|
}
|
|
48
47
|
export declare function startOfflineExecutor(config: OfflineConfig): OfflineExecutor;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
class
|
|
3
|
+
class WebOnlineDetector {
|
|
4
4
|
constructor() {
|
|
5
5
|
this.listeners = /* @__PURE__ */ new Set();
|
|
6
6
|
this.isListening = false;
|
|
@@ -69,5 +69,7 @@ class DefaultOnlineDetector {
|
|
|
69
69
|
this.listeners.clear();
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
+
const DefaultOnlineDetector = WebOnlineDetector;
|
|
72
73
|
exports.DefaultOnlineDetector = DefaultOnlineDetector;
|
|
74
|
+
exports.WebOnlineDetector = WebOnlineDetector;
|
|
73
75
|
//# sourceMappingURL=OnlineDetector.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OnlineDetector.cjs","sources":["../../../src/connectivity/OnlineDetector.ts"],"sourcesContent":["import type { OnlineDetector } from '../types'\n\nexport class
|
|
1
|
+
{"version":3,"file":"OnlineDetector.cjs","sources":["../../../src/connectivity/OnlineDetector.ts"],"sourcesContent":["import type { OnlineDetector } from '../types'\n\n/**\n * Web-based online detector that uses browser APIs.\n * Listens for:\n * - `window.online` event for network connectivity changes\n * - `document.visibilitychange` event for tab/window focus changes\n */\nexport class WebOnlineDetector implements OnlineDetector {\n private listeners: Set<() => void> = new Set()\n private isListening = false\n\n constructor() {\n this.startListening()\n }\n\n private startListening(): void {\n if (this.isListening) {\n return\n }\n\n this.isListening = true\n\n if (typeof window !== `undefined`) {\n window.addEventListener(`online`, this.handleOnline)\n document.addEventListener(`visibilitychange`, this.handleVisibilityChange)\n }\n }\n\n private stopListening(): void {\n if (!this.isListening) {\n return\n }\n\n this.isListening = false\n\n if (typeof window !== `undefined`) {\n window.removeEventListener(`online`, this.handleOnline)\n document.removeEventListener(\n `visibilitychange`,\n this.handleVisibilityChange,\n )\n }\n }\n\n private handleOnline = (): void => {\n this.notifyListeners()\n }\n\n private handleVisibilityChange = (): void => {\n if (document.visibilityState === `visible`) {\n this.notifyListeners()\n }\n }\n\n private notifyListeners(): void {\n for (const listener of this.listeners) {\n try {\n listener()\n } catch (error) {\n console.warn(`OnlineDetector listener error:`, error)\n }\n }\n }\n\n subscribe(callback: () => void): () => void {\n this.listeners.add(callback)\n\n return () => {\n this.listeners.delete(callback)\n\n if (this.listeners.size === 0) {\n this.stopListening()\n }\n }\n }\n\n notifyOnline(): void {\n this.notifyListeners()\n }\n\n isOnline(): boolean {\n if (typeof navigator !== `undefined`) {\n return navigator.onLine\n }\n return true\n }\n\n dispose(): void {\n this.stopListening()\n this.listeners.clear()\n }\n}\n\n/**\n * @deprecated Use `WebOnlineDetector` instead. This alias is kept for backwards compatibility.\n */\nexport const DefaultOnlineDetector = WebOnlineDetector\n"],"names":[],"mappings":";;AAQO,MAAM,kBAA4C;AAAA,EAIvD,cAAc;AAHd,SAAQ,gCAAiC,IAAA;AACzC,SAAQ,cAAc;AAmCtB,SAAQ,eAAe,MAAY;AACjC,WAAK,gBAAA;AAAA,IACP;AAEA,SAAQ,yBAAyB,MAAY;AAC3C,UAAI,SAAS,oBAAoB,WAAW;AAC1C,aAAK,gBAAA;AAAA,MACP;AAAA,IACF;AAxCE,SAAK,eAAA;AAAA,EACP;AAAA,EAEQ,iBAAuB;AAC7B,QAAI,KAAK,aAAa;AACpB;AAAA,IACF;AAEA,SAAK,cAAc;AAEnB,QAAI,OAAO,WAAW,aAAa;AACjC,aAAO,iBAAiB,UAAU,KAAK,YAAY;AACnD,eAAS,iBAAiB,oBAAoB,KAAK,sBAAsB;AAAA,IAC3E;AAAA,EACF;AAAA,EAEQ,gBAAsB;AAC5B,QAAI,CAAC,KAAK,aAAa;AACrB;AAAA,IACF;AAEA,SAAK,cAAc;AAEnB,QAAI,OAAO,WAAW,aAAa;AACjC,aAAO,oBAAoB,UAAU,KAAK,YAAY;AACtD,eAAS;AAAA,QACP;AAAA,QACA,KAAK;AAAA,MAAA;AAAA,IAET;AAAA,EACF;AAAA,EAYQ,kBAAwB;AAC9B,eAAW,YAAY,KAAK,WAAW;AACrC,UAAI;AACF,iBAAA;AAAA,MACF,SAAS,OAAO;AACd,gBAAQ,KAAK,kCAAkC,KAAK;AAAA,MACtD;AAAA,IACF;AAAA,EACF;AAAA,EAEA,UAAU,UAAkC;AAC1C,SAAK,UAAU,IAAI,QAAQ;AAE3B,WAAO,MAAM;AACX,WAAK,UAAU,OAAO,QAAQ;AAE9B,UAAI,KAAK,UAAU,SAAS,GAAG;AAC7B,aAAK,cAAA;AAAA,MACP;AAAA,IACF;AAAA,EACF;AAAA,EAEA,eAAqB;AACnB,SAAK,gBAAA;AAAA,EACP;AAAA,EAEA,WAAoB;AAClB,QAAI,OAAO,cAAc,aAAa;AACpC,aAAO,UAAU;AAAA,IACnB;AACA,WAAO;AAAA,EACT;AAAA,EAEA,UAAgB;AACd,SAAK,cAAA;AACL,SAAK,UAAU,MAAA;AAAA,EACjB;AACF;AAKO,MAAM,wBAAwB;;;"}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { OnlineDetector } from '../types.cjs';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Web-based online detector that uses browser APIs.
|
|
4
|
+
* Listens for:
|
|
5
|
+
* - `window.online` event for network connectivity changes
|
|
6
|
+
* - `document.visibilitychange` event for tab/window focus changes
|
|
7
|
+
*/
|
|
8
|
+
export declare class WebOnlineDetector implements OnlineDetector {
|
|
3
9
|
private listeners;
|
|
4
10
|
private isListening;
|
|
5
11
|
constructor();
|
|
@@ -13,3 +19,7 @@ export declare class DefaultOnlineDetector implements OnlineDetector {
|
|
|
13
19
|
isOnline(): boolean;
|
|
14
20
|
dispose(): void;
|
|
15
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* @deprecated Use `WebOnlineDetector` instead. This alias is kept for backwards compatibility.
|
|
24
|
+
*/
|
|
25
|
+
export declare const DefaultOnlineDetector: typeof WebOnlineDetector;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const NetInfo = require("@react-native-community/netinfo");
|
|
4
|
+
const reactNative = require("react-native");
|
|
5
|
+
class ReactNativeOnlineDetector {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
8
|
+
this.netInfoUnsubscribe = null;
|
|
9
|
+
this.appStateSubscription = null;
|
|
10
|
+
this.isListening = false;
|
|
11
|
+
this.wasConnected = true;
|
|
12
|
+
this.handleAppStateChange = (nextState) => {
|
|
13
|
+
if (nextState === `active`) {
|
|
14
|
+
this.notifyListeners();
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
this.startListening();
|
|
18
|
+
}
|
|
19
|
+
startListening() {
|
|
20
|
+
if (this.isListening) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
this.isListening = true;
|
|
24
|
+
this.netInfoUnsubscribe = NetInfo.addEventListener((state) => {
|
|
25
|
+
const isConnected = state.isConnected === true && state.isInternetReachable !== false;
|
|
26
|
+
if (isConnected && !this.wasConnected) {
|
|
27
|
+
this.notifyListeners();
|
|
28
|
+
}
|
|
29
|
+
this.wasConnected = isConnected;
|
|
30
|
+
});
|
|
31
|
+
this.appStateSubscription = reactNative.AppState.addEventListener(
|
|
32
|
+
`change`,
|
|
33
|
+
this.handleAppStateChange
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
stopListening() {
|
|
37
|
+
if (!this.isListening) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.isListening = false;
|
|
41
|
+
if (this.netInfoUnsubscribe) {
|
|
42
|
+
this.netInfoUnsubscribe();
|
|
43
|
+
this.netInfoUnsubscribe = null;
|
|
44
|
+
}
|
|
45
|
+
if (this.appStateSubscription) {
|
|
46
|
+
this.appStateSubscription.remove();
|
|
47
|
+
this.appStateSubscription = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
notifyListeners() {
|
|
51
|
+
for (const listener of this.listeners) {
|
|
52
|
+
try {
|
|
53
|
+
listener();
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.warn(`ReactNativeOnlineDetector listener error:`, error);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
subscribe(callback) {
|
|
60
|
+
this.listeners.add(callback);
|
|
61
|
+
return () => {
|
|
62
|
+
this.listeners.delete(callback);
|
|
63
|
+
if (this.listeners.size === 0) {
|
|
64
|
+
this.stopListening();
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
notifyOnline() {
|
|
69
|
+
this.notifyListeners();
|
|
70
|
+
}
|
|
71
|
+
dispose() {
|
|
72
|
+
this.stopListening();
|
|
73
|
+
this.listeners.clear();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
exports.ReactNativeOnlineDetector = ReactNativeOnlineDetector;
|
|
77
|
+
//# sourceMappingURL=ReactNativeOnlineDetector.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReactNativeOnlineDetector.cjs","sources":["../../../src/connectivity/ReactNativeOnlineDetector.ts"],"sourcesContent":["import NetInfo from '@react-native-community/netinfo'\nimport { AppState } from 'react-native'\nimport type { AppStateStatus, NativeEventSubscription } from 'react-native'\nimport type { OnlineDetector } from '../types'\n\n/**\n * React Native online detector that uses RN APIs.\n * Listens for:\n * - Network connectivity changes via `@react-native-community/netinfo`\n * - App state changes (foreground/background) via `AppState`\n */\nexport class ReactNativeOnlineDetector implements OnlineDetector {\n private listeners: Set<() => void> = new Set()\n private netInfoUnsubscribe: (() => void) | null = null\n private appStateSubscription: NativeEventSubscription | null = null\n private isListening = false\n private wasConnected = true\n\n constructor() {\n this.startListening()\n }\n\n private startListening(): void {\n if (this.isListening) {\n return\n }\n\n this.isListening = true\n\n // Subscribe to network state changes\n this.netInfoUnsubscribe = NetInfo.addEventListener((state) => {\n const isConnected =\n state.isConnected === true && state.isInternetReachable !== false\n\n // Only notify when transitioning to online\n if (isConnected && !this.wasConnected) {\n this.notifyListeners()\n }\n\n this.wasConnected = isConnected\n })\n\n // Subscribe to app state changes (foreground/background)\n this.appStateSubscription = AppState.addEventListener(\n `change`,\n this.handleAppStateChange,\n )\n }\n\n private handleAppStateChange = (nextState: AppStateStatus): void => {\n // Notify when app becomes active (foreground)\n if (nextState === `active`) {\n this.notifyListeners()\n }\n }\n\n private stopListening(): void {\n if (!this.isListening) {\n return\n }\n\n this.isListening = false\n\n if (this.netInfoUnsubscribe) {\n this.netInfoUnsubscribe()\n this.netInfoUnsubscribe = null\n }\n\n if (this.appStateSubscription) {\n this.appStateSubscription.remove()\n this.appStateSubscription = null\n }\n }\n\n private notifyListeners(): void {\n for (const listener of this.listeners) {\n try {\n listener()\n } catch (error) {\n console.warn(`ReactNativeOnlineDetector listener error:`, error)\n }\n }\n }\n\n subscribe(callback: () => void): () => void {\n this.listeners.add(callback)\n\n return () => {\n this.listeners.delete(callback)\n\n if (this.listeners.size === 0) {\n this.stopListening()\n }\n }\n }\n\n notifyOnline(): void {\n this.notifyListeners()\n }\n\n dispose(): void {\n this.stopListening()\n this.listeners.clear()\n }\n}\n"],"names":["AppState"],"mappings":";;;;AAWO,MAAM,0BAAoD;AAAA,EAO/D,cAAc;AANd,SAAQ,gCAAiC,IAAA;AACzC,SAAQ,qBAA0C;AAClD,SAAQ,uBAAuD;AAC/D,SAAQ,cAAc;AACtB,SAAQ,eAAe;AAiCvB,SAAQ,uBAAuB,CAAC,cAAoC;AAElE,UAAI,cAAc,UAAU;AAC1B,aAAK,gBAAA;AAAA,MACP;AAAA,IACF;AAnCE,SAAK,eAAA;AAAA,EACP;AAAA,EAEQ,iBAAuB;AAC7B,QAAI,KAAK,aAAa;AACpB;AAAA,IACF;AAEA,SAAK,cAAc;AAGnB,SAAK,qBAAqB,QAAQ,iBAAiB,CAAC,UAAU;AAC5D,YAAM,cACJ,MAAM,gBAAgB,QAAQ,MAAM,wBAAwB;AAG9D,UAAI,eAAe,CAAC,KAAK,cAAc;AACrC,aAAK,gBAAA;AAAA,MACP;AAEA,WAAK,eAAe;AAAA,IACtB,CAAC;AAGD,SAAK,uBAAuBA,YAAAA,SAAS;AAAA,MACnC;AAAA,MACA,KAAK;AAAA,IAAA;AAAA,EAET;AAAA,EASQ,gBAAsB;AAC5B,QAAI,CAAC,KAAK,aAAa;AACrB;AAAA,IACF;AAEA,SAAK,cAAc;AAEnB,QAAI,KAAK,oBAAoB;AAC3B,WAAK,mBAAA;AACL,WAAK,qBAAqB;AAAA,IAC5B;AAEA,QAAI,KAAK,sBAAsB;AAC7B,WAAK,qBAAqB,OAAA;AAC1B,WAAK,uBAAuB;AAAA,IAC9B;AAAA,EACF;AAAA,EAEQ,kBAAwB;AAC9B,eAAW,YAAY,KAAK,WAAW;AACrC,UAAI;AACF,iBAAA;AAAA,MACF,SAAS,OAAO;AACd,gBAAQ,KAAK,6CAA6C,KAAK;AAAA,MACjE;AAAA,IACF;AAAA,EACF;AAAA,EAEA,UAAU,UAAkC;AAC1C,SAAK,UAAU,IAAI,QAAQ;AAE3B,WAAO,MAAM;AACX,WAAK,UAAU,OAAO,QAAQ;AAE9B,UAAI,KAAK,UAAU,SAAS,GAAG;AAC7B,aAAK,cAAA;AAAA,MACP;AAAA,IACF;AAAA,EACF;AAAA,EAEA,eAAqB;AACnB,SAAK,gBAAA;AAAA,EACP;AAAA,EAEA,UAAgB;AACd,SAAK,cAAA;AACL,SAAK,UAAU,MAAA;AAAA,EACjB;AACF;;"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { OnlineDetector } from '../types.cjs';
|
|
2
|
+
/**
|
|
3
|
+
* React Native online detector that uses RN APIs.
|
|
4
|
+
* Listens for:
|
|
5
|
+
* - Network connectivity changes via `@react-native-community/netinfo`
|
|
6
|
+
* - App state changes (foreground/background) via `AppState`
|
|
7
|
+
*/
|
|
8
|
+
export declare class ReactNativeOnlineDetector implements OnlineDetector {
|
|
9
|
+
private listeners;
|
|
10
|
+
private netInfoUnsubscribe;
|
|
11
|
+
private appStateSubscription;
|
|
12
|
+
private isListening;
|
|
13
|
+
private wasConnected;
|
|
14
|
+
constructor();
|
|
15
|
+
private startListening;
|
|
16
|
+
private handleAppStateChange;
|
|
17
|
+
private stopListening;
|
|
18
|
+
private notifyListeners;
|
|
19
|
+
subscribe(callback: () => void): () => void;
|
|
20
|
+
notifyOnline(): void;
|
|
21
|
+
dispose(): void;
|
|
22
|
+
}
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -25,6 +25,7 @@ exports.BackoffCalculator = BackoffCalculator.BackoffCalculator;
|
|
|
25
25
|
exports.WebLocksLeader = WebLocksLeader.WebLocksLeader;
|
|
26
26
|
exports.BroadcastChannelLeader = BroadcastChannelLeader.BroadcastChannelLeader;
|
|
27
27
|
exports.DefaultOnlineDetector = OnlineDetector.DefaultOnlineDetector;
|
|
28
|
+
exports.WebOnlineDetector = OnlineDetector.WebOnlineDetector;
|
|
28
29
|
exports.OfflineTransactionAPI = OfflineTransaction.OfflineTransaction;
|
|
29
30
|
exports.createOfflineAction = OfflineAction.createOfflineAction;
|
|
30
31
|
exports.OutboxManager = OutboxManager.OutboxManager;
|
package/dist/cjs/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/cjs/index.d.cts
CHANGED
|
@@ -7,7 +7,7 @@ export { DefaultRetryPolicy } from './retry/RetryPolicy.cjs';
|
|
|
7
7
|
export { BackoffCalculator } from './retry/BackoffCalculator.cjs';
|
|
8
8
|
export { WebLocksLeader } from './coordination/WebLocksLeader.cjs';
|
|
9
9
|
export { BroadcastChannelLeader } from './coordination/BroadcastChannelLeader.cjs';
|
|
10
|
-
export { DefaultOnlineDetector } from './connectivity/OnlineDetector.cjs';
|
|
10
|
+
export { WebOnlineDetector, DefaultOnlineDetector, } from './connectivity/OnlineDetector.cjs';
|
|
11
11
|
export { OfflineTransaction as OfflineTransactionAPI } from './api/OfflineTransaction.cjs';
|
|
12
12
|
export { createOfflineAction } from './api/OfflineAction.cjs';
|
|
13
13
|
export { OutboxManager } from './outbox/OutboxManager.cjs';
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
class TransactionSerializer {
|
|
4
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
4
|
constructor(collections) {
|
|
6
5
|
this.collections = collections;
|
|
7
6
|
this.collectionIdToKey = /* @__PURE__ */ new Map();
|
|
@@ -12,30 +11,24 @@ class TransactionSerializer {
|
|
|
12
11
|
serialize(transaction) {
|
|
13
12
|
const serialized = {
|
|
14
13
|
...transaction,
|
|
15
|
-
createdAt: transaction.createdAt,
|
|
14
|
+
createdAt: transaction.createdAt.toISOString(),
|
|
16
15
|
mutations: transaction.mutations.map(
|
|
17
16
|
(mutation) => this.serializeMutation(mutation)
|
|
18
17
|
)
|
|
19
18
|
};
|
|
20
|
-
return JSON.stringify(serialized
|
|
21
|
-
if (value instanceof Date) {
|
|
22
|
-
return value.toISOString();
|
|
23
|
-
}
|
|
24
|
-
return value;
|
|
25
|
-
});
|
|
19
|
+
return JSON.stringify(serialized);
|
|
26
20
|
}
|
|
27
21
|
deserialize(data) {
|
|
28
|
-
const parsed = JSON.parse(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
);
|
|
22
|
+
const parsed = JSON.parse(data);
|
|
23
|
+
const createdAt = new Date(parsed.createdAt);
|
|
24
|
+
if (isNaN(createdAt.getTime())) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Failed to deserialize transaction: invalid createdAt value "${parsed.createdAt}"`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
37
29
|
return {
|
|
38
30
|
...parsed,
|
|
31
|
+
createdAt,
|
|
39
32
|
mutations: parsed.mutations.map(
|
|
40
33
|
(mutationData) => this.deserializeMutation(mutationData)
|
|
41
34
|
)
|
|
@@ -53,6 +46,7 @@ class TransactionSerializer {
|
|
|
53
46
|
type: mutation.type,
|
|
54
47
|
modified: this.serializeValue(mutation.modified),
|
|
55
48
|
original: this.serializeValue(mutation.original),
|
|
49
|
+
changes: this.serializeValue(mutation.changes),
|
|
56
50
|
collectionId: registryKey
|
|
57
51
|
// Store registry key instead of collection.id
|
|
58
52
|
};
|
|
@@ -67,14 +61,13 @@ class TransactionSerializer {
|
|
|
67
61
|
type: data.type,
|
|
68
62
|
modified: this.deserializeValue(data.modified),
|
|
69
63
|
original: this.deserializeValue(data.original),
|
|
64
|
+
changes: this.deserializeValue(data.changes) ?? {},
|
|
70
65
|
collection,
|
|
71
66
|
// These fields would need to be reconstructed by the executor
|
|
72
67
|
mutationId: ``,
|
|
73
68
|
// Will be regenerated
|
|
74
69
|
key: null,
|
|
75
70
|
// Will be extracted from the data
|
|
76
|
-
changes: {},
|
|
77
|
-
// Will be recalculated
|
|
78
71
|
metadata: void 0,
|
|
79
72
|
syncMetadata: {},
|
|
80
73
|
optimistic: true,
|
|
@@ -92,7 +85,7 @@ class TransactionSerializer {
|
|
|
92
85
|
if (typeof value === `object`) {
|
|
93
86
|
const result = Array.isArray(value) ? [] : {};
|
|
94
87
|
for (const key in value) {
|
|
95
|
-
if (
|
|
88
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
96
89
|
result[key] = this.serializeValue(value[key]);
|
|
97
90
|
}
|
|
98
91
|
}
|
|
@@ -105,12 +98,21 @@ class TransactionSerializer {
|
|
|
105
98
|
return value;
|
|
106
99
|
}
|
|
107
100
|
if (typeof value === `object` && value.__type === `Date`) {
|
|
108
|
-
|
|
101
|
+
if (value.value === void 0 || value.value === null) {
|
|
102
|
+
throw new Error(`Corrupted Date marker: missing value field`);
|
|
103
|
+
}
|
|
104
|
+
const date = new Date(value.value);
|
|
105
|
+
if (isNaN(date.getTime())) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Failed to deserialize Date marker: invalid date value "${value.value}"`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return date;
|
|
109
111
|
}
|
|
110
112
|
if (typeof value === `object`) {
|
|
111
113
|
const result = Array.isArray(value) ? [] : {};
|
|
112
114
|
for (const key in value) {
|
|
113
|
-
if (
|
|
115
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
114
116
|
result[key] = this.deserializeValue(value[key]);
|
|
115
117
|
}
|
|
116
118
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TransactionSerializer.cjs","sources":["../../../src/outbox/TransactionSerializer.ts"],"sourcesContent":["import type {\n OfflineTransaction,\n SerializedError,\n SerializedMutation,\n SerializedOfflineTransaction,\n} from '../types'\nimport type { Collection, PendingMutation } from '@tanstack/db'\n\nexport class TransactionSerializer {\n
|
|
1
|
+
{"version":3,"file":"TransactionSerializer.cjs","sources":["../../../src/outbox/TransactionSerializer.ts"],"sourcesContent":["import type {\n OfflineTransaction,\n SerializedError,\n SerializedMutation,\n SerializedOfflineTransaction,\n} from '../types'\nimport type { Collection, PendingMutation } from '@tanstack/db'\n\nexport class TransactionSerializer {\n private collections: Record<string, Collection<any, any, any, any, any>>\n private collectionIdToKey: Map<string, string>\n\n constructor(\n collections: Record<string, Collection<any, any, any, any, any>>,\n ) {\n this.collections = collections\n // Create reverse lookup from collection.id to registry key\n this.collectionIdToKey = new Map()\n for (const [key, collection] of Object.entries(collections)) {\n this.collectionIdToKey.set(collection.id, key)\n }\n }\n\n serialize(transaction: OfflineTransaction): string {\n const serialized: SerializedOfflineTransaction = {\n ...transaction,\n createdAt: transaction.createdAt.toISOString(),\n mutations: transaction.mutations.map((mutation) =>\n this.serializeMutation(mutation),\n ),\n }\n return JSON.stringify(serialized)\n }\n\n deserialize(data: string): OfflineTransaction {\n // Parse without a reviver - let deserializeValue handle dates in mutation data\n // using the { __type: 'Date' } marker system\n const parsed: SerializedOfflineTransaction = JSON.parse(data)\n\n const createdAt = new Date(parsed.createdAt)\n if (isNaN(createdAt.getTime())) {\n throw new Error(\n `Failed to deserialize transaction: invalid createdAt value \"${parsed.createdAt}\"`,\n )\n }\n\n return {\n ...parsed,\n createdAt,\n mutations: parsed.mutations.map((mutationData) =>\n this.deserializeMutation(mutationData),\n ),\n }\n }\n\n private serializeMutation(mutation: PendingMutation): SerializedMutation {\n const registryKey = this.collectionIdToKey.get(mutation.collection.id)\n if (!registryKey) {\n throw new Error(\n `Collection with id ${mutation.collection.id} not found in registry`,\n )\n }\n\n return {\n globalKey: mutation.globalKey,\n type: mutation.type,\n modified: this.serializeValue(mutation.modified),\n original: this.serializeValue(mutation.original),\n changes: this.serializeValue(mutation.changes),\n collectionId: registryKey, // Store registry key instead of collection.id\n }\n }\n\n private deserializeMutation(data: SerializedMutation): PendingMutation {\n const collection = this.collections[data.collectionId]\n if (!collection) {\n throw new Error(`Collection with id ${data.collectionId} not found`)\n }\n\n // Create a partial PendingMutation - we can't fully reconstruct it but\n // we provide what we can. The executor will need to handle the rest.\n return {\n globalKey: data.globalKey,\n type: data.type as any,\n modified: this.deserializeValue(data.modified),\n original: this.deserializeValue(data.original),\n changes: this.deserializeValue(data.changes) ?? {},\n collection,\n // These fields would need to be reconstructed by the executor\n mutationId: ``, // Will be regenerated\n key: null, // Will be extracted from the data\n metadata: undefined,\n syncMetadata: {},\n optimistic: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n } as PendingMutation\n }\n\n private serializeValue(value: any): any {\n if (value === null || value === undefined) {\n return value\n }\n\n if (value instanceof Date) {\n return { __type: `Date`, value: value.toISOString() }\n }\n\n if (typeof value === `object`) {\n const result: any = Array.isArray(value) ? [] : {}\n for (const key in value) {\n if (Object.prototype.hasOwnProperty.call(value, key)) {\n result[key] = this.serializeValue(value[key])\n }\n }\n return result\n }\n\n return value\n }\n\n private deserializeValue(value: any): any {\n if (value === null || value === undefined) {\n return value\n }\n\n if (typeof value === `object` && value.__type === `Date`) {\n if (value.value === undefined || value.value === null) {\n throw new Error(`Corrupted Date marker: missing value field`)\n }\n const date = new Date(value.value)\n if (isNaN(date.getTime())) {\n throw new Error(\n `Failed to deserialize Date marker: invalid date value \"${value.value}\"`,\n )\n }\n return date\n }\n\n if (typeof value === `object`) {\n const result: any = Array.isArray(value) ? [] : {}\n for (const key in value) {\n if (Object.prototype.hasOwnProperty.call(value, key)) {\n result[key] = this.deserializeValue(value[key])\n }\n }\n return result\n }\n\n return value\n }\n\n serializeError(error: Error): SerializedError {\n return {\n name: error.name,\n message: error.message,\n stack: error.stack,\n }\n }\n\n deserializeError(data: SerializedError): Error {\n const error = new Error(data.message)\n error.name = data.name\n error.stack = data.stack\n return error\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const OfflineExecutor$1 = require("../OfflineExecutor.cjs");
|
|
4
|
+
const ReactNativeOnlineDetector = require("../connectivity/ReactNativeOnlineDetector.cjs");
|
|
5
|
+
class OfflineExecutor extends OfflineExecutor$1.OfflineExecutor {
|
|
6
|
+
constructor(config) {
|
|
7
|
+
super({
|
|
8
|
+
...config,
|
|
9
|
+
onlineDetector: config.onlineDetector ?? new ReactNativeOnlineDetector.ReactNativeOnlineDetector()
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function startOfflineExecutor(config) {
|
|
14
|
+
return new OfflineExecutor(config);
|
|
15
|
+
}
|
|
16
|
+
exports.OfflineExecutor = OfflineExecutor;
|
|
17
|
+
exports.startOfflineExecutor = startOfflineExecutor;
|
|
18
|
+
//# sourceMappingURL=OfflineExecutor.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OfflineExecutor.cjs","sources":["../../../src/react-native/OfflineExecutor.ts"],"sourcesContent":["import { OfflineExecutor as BaseOfflineExecutor } from '../OfflineExecutor'\nimport { ReactNativeOnlineDetector } from '../connectivity/ReactNativeOnlineDetector'\nimport type { OfflineConfig } from '../types'\n\n/**\n * OfflineExecutor configured for React Native environments.\n * Uses ReactNativeOnlineDetector by default instead of WebOnlineDetector.\n */\nexport class OfflineExecutor extends BaseOfflineExecutor {\n constructor(config: OfflineConfig) {\n super({\n ...config,\n onlineDetector: config.onlineDetector ?? new ReactNativeOnlineDetector(),\n })\n }\n}\n\n/**\n * Start an offline executor configured for React Native environments.\n * Uses ReactNativeOnlineDetector by default instead of WebOnlineDetector.\n */\nexport function startOfflineExecutor(config: OfflineConfig): OfflineExecutor {\n return new OfflineExecutor(config)\n}\n"],"names":["BaseOfflineExecutor","ReactNativeOnlineDetector"],"mappings":";;;;AAQO,MAAM,wBAAwBA,kBAAAA,gBAAoB;AAAA,EACvD,YAAY,QAAuB;AACjC,UAAM;AAAA,MACJ,GAAG;AAAA,MACH,gBAAgB,OAAO,kBAAkB,IAAIC,0BAAAA,0BAAA;AAAA,IAA0B,CACxE;AAAA,EACH;AACF;AAMO,SAAS,qBAAqB,QAAwC;AAC3E,SAAO,IAAI,gBAAgB,MAAM;AACnC;;;"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { OfflineExecutor as BaseOfflineExecutor } from '../OfflineExecutor.cjs';
|
|
2
|
+
import { OfflineConfig } from '../types.cjs';
|
|
3
|
+
/**
|
|
4
|
+
* OfflineExecutor configured for React Native environments.
|
|
5
|
+
* Uses ReactNativeOnlineDetector by default instead of WebOnlineDetector.
|
|
6
|
+
*/
|
|
7
|
+
export declare class OfflineExecutor extends BaseOfflineExecutor {
|
|
8
|
+
constructor(config: OfflineConfig);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Start an offline executor configured for React Native environments.
|
|
12
|
+
* Uses ReactNativeOnlineDetector by default instead of WebOnlineDetector.
|
|
13
|
+
*/
|
|
14
|
+
export declare function startOfflineExecutor(config: OfflineConfig): OfflineExecutor;
|