@tanstack/offline-transactions 1.0.24 → 1.0.25
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.
|
@@ -136,12 +136,31 @@ let OfflineExecutor$1 = class OfflineExecutor {
|
|
|
136
136
|
this.unsubscribeOnline = this.onlineDetector.subscribe(() => {
|
|
137
137
|
if (this.isOfflineEnabled && this.executor) {
|
|
138
138
|
this.executor.resetRetryDelays();
|
|
139
|
-
this.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
139
|
+
if (this.scheduler.getPendingCount() > 0) {
|
|
140
|
+
const barrierPromise = this.executor.executeAll();
|
|
141
|
+
for (const collection of Object.values(this.config.collections)) {
|
|
142
|
+
collection.deferDataRefresh = barrierPromise;
|
|
143
|
+
}
|
|
144
|
+
barrierPromise.catch((error) => {
|
|
145
|
+
console.warn(
|
|
146
|
+
`Failed to execute transactions on connectivity change:`,
|
|
147
|
+
error
|
|
148
|
+
);
|
|
149
|
+
}).finally(() => {
|
|
150
|
+
for (const collection of Object.values(this.config.collections)) {
|
|
151
|
+
if (collection.deferDataRefresh === barrierPromise) {
|
|
152
|
+
collection.deferDataRefresh = null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
} else {
|
|
157
|
+
this.executor.executeAll().catch((error) => {
|
|
158
|
+
console.warn(
|
|
159
|
+
`Failed to execute transactions on connectivity change:`,
|
|
160
|
+
error
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
145
164
|
}
|
|
146
165
|
});
|
|
147
166
|
}
|
|
@@ -393,6 +412,9 @@ let OfflineExecutor$1 = class OfflineExecutor {
|
|
|
393
412
|
return this.onlineDetector.isOnline();
|
|
394
413
|
}
|
|
395
414
|
dispose() {
|
|
415
|
+
for (const collection of Object.values(this.config.collections)) {
|
|
416
|
+
collection.deferDataRefresh = null;
|
|
417
|
+
}
|
|
396
418
|
if (this.unsubscribeOnline) {
|
|
397
419
|
this.unsubscribeOnline();
|
|
398
420
|
this.unsubscribeOnline = null;
|
|
@@ -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 { 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 // Track restoration transactions for cleanup when offline transactions complete\n private restorationTransactions: Map<string, Transaction> = 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 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 // Load pending transactions and restore optimistic state\n await this.executor.loadPendingTransactions()\n\n // Start execution in the background - don't await to avoid blocking initialization\n // The transactions will execute and complete asynchronously\n this.executor.executeAll().catch((error) => {\n console.warn(`Failed to execute transactions:`, error)\n })\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 /**\n * Wait for the executor to fully initialize.\n * This ensures that pending transactions are loaded and optimistic state is restored.\n */\n async waitForInit(): Promise<void> {\n return this.initPromise\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 // Clean up the restoration transaction - the sync will provide authoritative data\n this.cleanupRestorationTransaction(transactionId)\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 // Clean up the restoration transaction and rollback optimistic state\n this.cleanupRestorationTransaction(transactionId, true)\n }\n\n // Method for TransactionExecutor to register restoration transactions\n registerRestorationTransaction(\n offlineTransactionId: string,\n restorationTransaction: Transaction,\n ): void {\n this.restorationTransactions.set(\n offlineTransactionId,\n restorationTransaction,\n )\n }\n\n private cleanupRestorationTransaction(\n transactionId: string,\n shouldRollback = false,\n ): void {\n const restorationTx = this.restorationTransactions.get(transactionId)\n if (!restorationTx) {\n return\n }\n\n this.restorationTransactions.delete(transactionId)\n\n if (shouldRollback) {\n restorationTx.rollback()\n return\n }\n\n // Mark as completed so recomputeOptimisticState removes it from consideration.\n // The actual data will come from the sync.\n restorationTx.setState(`completed`)\n\n // Remove from each collection's transaction map and recompute\n const touchedCollections = new Set<string>()\n for (const mutation of restorationTx.mutations) {\n // Defensive check for corrupted deserialized data\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!mutation.collection) {\n continue\n }\n const collectionId = mutation.collection.id\n if (touchedCollections.has(collectionId)) {\n continue\n }\n touchedCollections.add(collectionId)\n mutation.collection._state.transactions.delete(restorationTx.id)\n mutation.collection._state.recomputeOptimisticState(false)\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 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 isOnline(): boolean {\n return this.onlineDetector.isOnline()\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
|
+
{"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 // Track restoration transactions for cleanup when offline transactions complete\n private restorationTransactions: Map<string, Transaction> = 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 this.executor.resetRetryDelays()\n\n if (this.scheduler.getPendingCount() > 0) {\n const barrierPromise = this.executor.executeAll()\n\n for (const collection of Object.values(this.config.collections)) {\n collection.deferDataRefresh = barrierPromise\n }\n\n barrierPromise\n .catch((error) => {\n console.warn(\n `Failed to execute transactions on connectivity change:`,\n error,\n )\n })\n .finally(() => {\n for (const collection of Object.values(this.config.collections)) {\n if (collection.deferDataRefresh === barrierPromise) {\n collection.deferDataRefresh = null\n }\n }\n })\n } else {\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\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 // Load pending transactions and restore optimistic state\n await this.executor.loadPendingTransactions()\n\n // Start execution in the background - don't await to avoid blocking initialization\n // The transactions will execute and complete asynchronously\n this.executor.executeAll().catch((error) => {\n console.warn(`Failed to execute transactions:`, error)\n })\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 /**\n * Wait for the executor to fully initialize.\n * This ensures that pending transactions are loaded and optimistic state is restored.\n */\n async waitForInit(): Promise<void> {\n return this.initPromise\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 // Clean up the restoration transaction - the sync will provide authoritative data\n this.cleanupRestorationTransaction(transactionId)\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 // Clean up the restoration transaction and rollback optimistic state\n this.cleanupRestorationTransaction(transactionId, true)\n }\n\n // Method for TransactionExecutor to register restoration transactions\n registerRestorationTransaction(\n offlineTransactionId: string,\n restorationTransaction: Transaction,\n ): void {\n this.restorationTransactions.set(\n offlineTransactionId,\n restorationTransaction,\n )\n }\n\n private cleanupRestorationTransaction(\n transactionId: string,\n shouldRollback = false,\n ): void {\n const restorationTx = this.restorationTransactions.get(transactionId)\n if (!restorationTx) {\n return\n }\n\n this.restorationTransactions.delete(transactionId)\n\n if (shouldRollback) {\n restorationTx.rollback()\n return\n }\n\n // Mark as completed so recomputeOptimisticState removes it from consideration.\n // The actual data will come from the sync.\n restorationTx.setState(`completed`)\n\n // Remove from each collection's transaction map and recompute\n const touchedCollections = new Set<string>()\n for (const mutation of restorationTx.mutations) {\n // Defensive check for corrupted deserialized data\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!mutation.collection) {\n continue\n }\n const collectionId = mutation.collection.id\n if (touchedCollections.has(collectionId)) {\n continue\n }\n touchedCollections.add(collectionId)\n mutation.collection._state.transactions.delete(restorationTx.id)\n mutation.collection._state.recomputeOptimisticState(false)\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 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 isOnline(): boolean {\n return this.onlineDetector.isOnline()\n }\n\n dispose(): void {\n for (const collection of Object.values(this.config.collections)) {\n collection.deferDataRefresh = null\n }\n\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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"}
|
|
@@ -134,12 +134,31 @@ let OfflineExecutor$1 = class OfflineExecutor {
|
|
|
134
134
|
this.unsubscribeOnline = this.onlineDetector.subscribe(() => {
|
|
135
135
|
if (this.isOfflineEnabled && this.executor) {
|
|
136
136
|
this.executor.resetRetryDelays();
|
|
137
|
-
this.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
137
|
+
if (this.scheduler.getPendingCount() > 0) {
|
|
138
|
+
const barrierPromise = this.executor.executeAll();
|
|
139
|
+
for (const collection of Object.values(this.config.collections)) {
|
|
140
|
+
collection.deferDataRefresh = barrierPromise;
|
|
141
|
+
}
|
|
142
|
+
barrierPromise.catch((error) => {
|
|
143
|
+
console.warn(
|
|
144
|
+
`Failed to execute transactions on connectivity change:`,
|
|
145
|
+
error
|
|
146
|
+
);
|
|
147
|
+
}).finally(() => {
|
|
148
|
+
for (const collection of Object.values(this.config.collections)) {
|
|
149
|
+
if (collection.deferDataRefresh === barrierPromise) {
|
|
150
|
+
collection.deferDataRefresh = null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
} else {
|
|
155
|
+
this.executor.executeAll().catch((error) => {
|
|
156
|
+
console.warn(
|
|
157
|
+
`Failed to execute transactions on connectivity change:`,
|
|
158
|
+
error
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
143
162
|
}
|
|
144
163
|
});
|
|
145
164
|
}
|
|
@@ -391,6 +410,9 @@ let OfflineExecutor$1 = class OfflineExecutor {
|
|
|
391
410
|
return this.onlineDetector.isOnline();
|
|
392
411
|
}
|
|
393
412
|
dispose() {
|
|
413
|
+
for (const collection of Object.values(this.config.collections)) {
|
|
414
|
+
collection.deferDataRefresh = null;
|
|
415
|
+
}
|
|
394
416
|
if (this.unsubscribeOnline) {
|
|
395
417
|
this.unsubscribeOnline();
|
|
396
418
|
this.unsubscribeOnline = null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OfflineExecutor.js","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 // Track restoration transactions for cleanup when offline transactions complete\n private restorationTransactions: Map<string, Transaction> = 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 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 // Load pending transactions and restore optimistic state\n await this.executor.loadPendingTransactions()\n\n // Start execution in the background - don't await to avoid blocking initialization\n // The transactions will execute and complete asynchronously\n this.executor.executeAll().catch((error) => {\n console.warn(`Failed to execute transactions:`, error)\n })\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 /**\n * Wait for the executor to fully initialize.\n * This ensures that pending transactions are loaded and optimistic state is restored.\n */\n async waitForInit(): Promise<void> {\n return this.initPromise\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 // Clean up the restoration transaction - the sync will provide authoritative data\n this.cleanupRestorationTransaction(transactionId)\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 // Clean up the restoration transaction and rollback optimistic state\n this.cleanupRestorationTransaction(transactionId, true)\n }\n\n // Method for TransactionExecutor to register restoration transactions\n registerRestorationTransaction(\n offlineTransactionId: string,\n restorationTransaction: Transaction,\n ): void {\n this.restorationTransactions.set(\n offlineTransactionId,\n restorationTransaction,\n )\n }\n\n private cleanupRestorationTransaction(\n transactionId: string,\n shouldRollback = false,\n ): void {\n const restorationTx = this.restorationTransactions.get(transactionId)\n if (!restorationTx) {\n return\n }\n\n this.restorationTransactions.delete(transactionId)\n\n if (shouldRollback) {\n restorationTx.rollback()\n return\n }\n\n // Mark as completed so recomputeOptimisticState removes it from consideration.\n // The actual data will come from the sync.\n restorationTx.setState(`completed`)\n\n // Remove from each collection's transaction map and recompute\n const touchedCollections = new Set<string>()\n for (const mutation of restorationTx.mutations) {\n // Defensive check for corrupted deserialized data\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!mutation.collection) {\n continue\n }\n const collectionId = mutation.collection.id\n if (touchedCollections.has(collectionId)) {\n continue\n }\n touchedCollections.add(collectionId)\n mutation.collection._state.transactions.delete(restorationTx.id)\n mutation.collection._state.recomputeOptimisticState(false)\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 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 isOnline(): boolean {\n return this.onlineDetector.isOnline()\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":["OfflineTransactionAPI","action","OfflineExecutor"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"OfflineExecutor.js","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 // Track restoration transactions for cleanup when offline transactions complete\n private restorationTransactions: Map<string, Transaction> = 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 this.executor.resetRetryDelays()\n\n if (this.scheduler.getPendingCount() > 0) {\n const barrierPromise = this.executor.executeAll()\n\n for (const collection of Object.values(this.config.collections)) {\n collection.deferDataRefresh = barrierPromise\n }\n\n barrierPromise\n .catch((error) => {\n console.warn(\n `Failed to execute transactions on connectivity change:`,\n error,\n )\n })\n .finally(() => {\n for (const collection of Object.values(this.config.collections)) {\n if (collection.deferDataRefresh === barrierPromise) {\n collection.deferDataRefresh = null\n }\n }\n })\n } else {\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\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 // Load pending transactions and restore optimistic state\n await this.executor.loadPendingTransactions()\n\n // Start execution in the background - don't await to avoid blocking initialization\n // The transactions will execute and complete asynchronously\n this.executor.executeAll().catch((error) => {\n console.warn(`Failed to execute transactions:`, error)\n })\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 /**\n * Wait for the executor to fully initialize.\n * This ensures that pending transactions are loaded and optimistic state is restored.\n */\n async waitForInit(): Promise<void> {\n return this.initPromise\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 // Clean up the restoration transaction - the sync will provide authoritative data\n this.cleanupRestorationTransaction(transactionId)\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 // Clean up the restoration transaction and rollback optimistic state\n this.cleanupRestorationTransaction(transactionId, true)\n }\n\n // Method for TransactionExecutor to register restoration transactions\n registerRestorationTransaction(\n offlineTransactionId: string,\n restorationTransaction: Transaction,\n ): void {\n this.restorationTransactions.set(\n offlineTransactionId,\n restorationTransaction,\n )\n }\n\n private cleanupRestorationTransaction(\n transactionId: string,\n shouldRollback = false,\n ): void {\n const restorationTx = this.restorationTransactions.get(transactionId)\n if (!restorationTx) {\n return\n }\n\n this.restorationTransactions.delete(transactionId)\n\n if (shouldRollback) {\n restorationTx.rollback()\n return\n }\n\n // Mark as completed so recomputeOptimisticState removes it from consideration.\n // The actual data will come from the sync.\n restorationTx.setState(`completed`)\n\n // Remove from each collection's transaction map and recompute\n const touchedCollections = new Set<string>()\n for (const mutation of restorationTx.mutations) {\n // Defensive check for corrupted deserialized data\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!mutation.collection) {\n continue\n }\n const collectionId = mutation.collection.id\n if (touchedCollections.has(collectionId)) {\n continue\n }\n touchedCollections.add(collectionId)\n mutation.collection._state.transactions.delete(restorationTx.id)\n mutation.collection._state.recomputeOptimisticState(false)\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 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 isOnline(): boolean {\n return this.onlineDetector.isOnline()\n }\n\n dispose(): void {\n for (const collection of Object.values(this.config.collections)) {\n collection.deferDataRefresh = null\n }\n\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":["OfflineTransactionAPI","action","OfflineExecutor"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/offline-transactions",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.25",
|
|
4
4
|
"description": "Offline-first transaction capabilities for TanStack DB",
|
|
5
5
|
"author": "TanStack",
|
|
6
6
|
"license": "MIT",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"skills"
|
|
52
52
|
],
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@tanstack/db": "0.
|
|
54
|
+
"@tanstack/db": "0.6.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"@react-native-community/netinfo": ">=11.0.0",
|
package/src/OfflineExecutor.ts
CHANGED
|
@@ -221,12 +221,36 @@ export class OfflineExecutor {
|
|
|
221
221
|
this.unsubscribeOnline = this.onlineDetector.subscribe(() => {
|
|
222
222
|
if (this.isOfflineEnabled && this.executor) {
|
|
223
223
|
this.executor.resetRetryDelays()
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
)
|
|
229
|
-
|
|
224
|
+
|
|
225
|
+
if (this.scheduler.getPendingCount() > 0) {
|
|
226
|
+
const barrierPromise = this.executor.executeAll()
|
|
227
|
+
|
|
228
|
+
for (const collection of Object.values(this.config.collections)) {
|
|
229
|
+
collection.deferDataRefresh = barrierPromise
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
barrierPromise
|
|
233
|
+
.catch((error) => {
|
|
234
|
+
console.warn(
|
|
235
|
+
`Failed to execute transactions on connectivity change:`,
|
|
236
|
+
error,
|
|
237
|
+
)
|
|
238
|
+
})
|
|
239
|
+
.finally(() => {
|
|
240
|
+
for (const collection of Object.values(this.config.collections)) {
|
|
241
|
+
if (collection.deferDataRefresh === barrierPromise) {
|
|
242
|
+
collection.deferDataRefresh = null
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
} else {
|
|
247
|
+
this.executor.executeAll().catch((error) => {
|
|
248
|
+
console.warn(
|
|
249
|
+
`Failed to execute transactions on connectivity change:`,
|
|
250
|
+
error,
|
|
251
|
+
)
|
|
252
|
+
})
|
|
253
|
+
}
|
|
230
254
|
}
|
|
231
255
|
})
|
|
232
256
|
}
|
|
@@ -568,6 +592,10 @@ export class OfflineExecutor {
|
|
|
568
592
|
}
|
|
569
593
|
|
|
570
594
|
dispose(): void {
|
|
595
|
+
for (const collection of Object.values(this.config.collections)) {
|
|
596
|
+
collection.deferDataRefresh = null
|
|
597
|
+
}
|
|
598
|
+
|
|
571
599
|
if (this.unsubscribeOnline) {
|
|
572
600
|
this.unsubscribeOnline()
|
|
573
601
|
this.unsubscribeOnline = null
|