@tanstack/offline-transactions 1.0.5 → 1.0.7
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/dist/cjs/OfflineExecutor.cjs +4 -0
- package/dist/cjs/OfflineExecutor.cjs.map +1 -1
- package/dist/cjs/coordination/WebLocksLeader.cjs +1 -0
- package/dist/cjs/coordination/WebLocksLeader.cjs.map +1 -1
- package/dist/esm/OfflineExecutor.js +4 -0
- package/dist/esm/OfflineExecutor.js.map +1 -1
- package/dist/esm/coordination/WebLocksLeader.js +1 -0
- package/dist/esm/coordination/WebLocksLeader.js.map +1 -1
- package/package.json +3 -3
- package/src/OfflineExecutor.ts +6 -0
- package/src/coordination/WebLocksLeader.ts +6 -0
|
@@ -170,8 +170,12 @@ class OfflineExecutor {
|
|
|
170
170
|
);
|
|
171
171
|
this.leaderElection = this.createLeaderElection();
|
|
172
172
|
const isLeader = await this.leaderElection.requestLeadership();
|
|
173
|
+
this.isLeaderState = isLeader;
|
|
173
174
|
span.setAttribute(`isLeader`, isLeader);
|
|
174
175
|
this.setupEventListeners();
|
|
176
|
+
if (this.config.onLeadershipChange) {
|
|
177
|
+
this.config.onLeadershipChange(isLeader);
|
|
178
|
+
}
|
|
175
179
|
if (isLeader) {
|
|
176
180
|
await this.loadAndReplayTransactions();
|
|
177
181
|
}
|
|
@@ -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 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 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 { 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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebLocksLeader.cjs","sources":["../../../src/coordination/WebLocksLeader.ts"],"sourcesContent":["import { BaseLeaderElection } from './LeaderElection'\n\nexport class WebLocksLeader extends BaseLeaderElection {\n private lockName: string\n private releaseLock: (() => void) | null = null\n\n constructor(lockName = `offline-executor-leader`) {\n super()\n this.lockName = lockName\n }\n\n async requestLeadership(): Promise<boolean> {\n if (!this.isWebLocksSupported()) {\n return false\n }\n\n if (this.isLeaderState) {\n return true\n }\n\n try {\n // First try to acquire the lock with ifAvailable\n const available = await navigator.locks.request(\n this.lockName,\n {\n mode: `exclusive`,\n ifAvailable: true,\n },\n (lock) => {\n return lock !== null\n },\n )\n\n if (!available) {\n return false\n }\n\n // Lock is available, now acquire it for real and hold it\n navigator.locks.request(\n this.lockName,\n {\n mode: `exclusive`,\n },\n async (lock) => {\n if (lock) {\n this.notifyLeadershipChange(true)\n // Hold the lock until released\n return new Promise<void>((resolve) => {\n this.releaseLock = () => {\n this.notifyLeadershipChange(false)\n resolve()\n }\n })\n }\n },\n )\n\n return true\n } catch (error) {\n if (error instanceof Error && error.name === `AbortError`) {\n return false\n }\n console.warn(`Web Locks leadership request failed:`, error)\n return false\n }\n }\n\n releaseLeadership(): void {\n if (this.releaseLock) {\n this.releaseLock()\n this.releaseLock = null\n }\n }\n\n private isWebLocksSupported(): boolean {\n return typeof navigator !== `undefined` && `locks` in navigator\n }\n\n static isSupported(): boolean {\n return typeof navigator !== `undefined` && `locks` in navigator\n }\n}\n"],"names":["BaseLeaderElection"],"mappings":";;;AAEO,MAAM,uBAAuBA,eAAAA,mBAAmB;AAAA,EAIrD,YAAY,WAAW,2BAA2B;AAChD,UAAA;AAHF,SAAQ,cAAmC;AAIzC,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,MAAM,oBAAsC;AAC1C,QAAI,CAAC,KAAK,uBAAuB;AAC/B,aAAO;AAAA,IACT;AAEA,QAAI,KAAK,eAAe;AACtB,aAAO;AAAA,IACT;AAEA,QAAI;AAEF,YAAM,YAAY,MAAM,UAAU,MAAM;AAAA,QACtC,KAAK;AAAA,QACL;AAAA,UACE,MAAM;AAAA,UACN,aAAa;AAAA,QAAA;AAAA,QAEf,CAAC,SAAS;AACR,iBAAO,SAAS;AAAA,QAClB;AAAA,MAAA;AAGF,UAAI,CAAC,WAAW;AACd,eAAO;AAAA,MACT;
|
|
1
|
+
{"version":3,"file":"WebLocksLeader.cjs","sources":["../../../src/coordination/WebLocksLeader.ts"],"sourcesContent":["import { BaseLeaderElection } from './LeaderElection'\n\nexport class WebLocksLeader extends BaseLeaderElection {\n private lockName: string\n private releaseLock: (() => void) | null = null\n\n constructor(lockName = `offline-executor-leader`) {\n super()\n this.lockName = lockName\n }\n\n async requestLeadership(): Promise<boolean> {\n if (!this.isWebLocksSupported()) {\n return false\n }\n\n if (this.isLeaderState) {\n return true\n }\n\n try {\n // First try to acquire the lock with ifAvailable\n const available = await navigator.locks.request(\n this.lockName,\n {\n mode: `exclusive`,\n ifAvailable: true,\n },\n (lock) => {\n return lock !== null\n },\n )\n\n if (!available) {\n return false\n }\n\n // Set state immediately to prevent duplicate notifications\n // when the async lock acquisition calls notifyLeadershipChange(true).\n // The guard in notifyLeadershipChange checks `isLeaderState !== isLeader`,\n // so setting this to true here prevents the callback from firing again.\n this.isLeaderState = true\n\n // Lock is available, now acquire it for real and hold it\n navigator.locks.request(\n this.lockName,\n {\n mode: `exclusive`,\n },\n async (lock) => {\n if (lock) {\n this.notifyLeadershipChange(true)\n // Hold the lock until released\n return new Promise<void>((resolve) => {\n this.releaseLock = () => {\n this.notifyLeadershipChange(false)\n resolve()\n }\n })\n }\n },\n )\n\n return true\n } catch (error) {\n if (error instanceof Error && error.name === `AbortError`) {\n return false\n }\n console.warn(`Web Locks leadership request failed:`, error)\n return false\n }\n }\n\n releaseLeadership(): void {\n if (this.releaseLock) {\n this.releaseLock()\n this.releaseLock = null\n }\n }\n\n private isWebLocksSupported(): boolean {\n return typeof navigator !== `undefined` && `locks` in navigator\n }\n\n static isSupported(): boolean {\n return typeof navigator !== `undefined` && `locks` in navigator\n }\n}\n"],"names":["BaseLeaderElection"],"mappings":";;;AAEO,MAAM,uBAAuBA,eAAAA,mBAAmB;AAAA,EAIrD,YAAY,WAAW,2BAA2B;AAChD,UAAA;AAHF,SAAQ,cAAmC;AAIzC,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,MAAM,oBAAsC;AAC1C,QAAI,CAAC,KAAK,uBAAuB;AAC/B,aAAO;AAAA,IACT;AAEA,QAAI,KAAK,eAAe;AACtB,aAAO;AAAA,IACT;AAEA,QAAI;AAEF,YAAM,YAAY,MAAM,UAAU,MAAM;AAAA,QACtC,KAAK;AAAA,QACL;AAAA,UACE,MAAM;AAAA,UACN,aAAa;AAAA,QAAA;AAAA,QAEf,CAAC,SAAS;AACR,iBAAO,SAAS;AAAA,QAClB;AAAA,MAAA;AAGF,UAAI,CAAC,WAAW;AACd,eAAO;AAAA,MACT;AAMA,WAAK,gBAAgB;AAGrB,gBAAU,MAAM;AAAA,QACd,KAAK;AAAA,QACL;AAAA,UACE,MAAM;AAAA,QAAA;AAAA,QAER,OAAO,SAAS;AACd,cAAI,MAAM;AACR,iBAAK,uBAAuB,IAAI;AAEhC,mBAAO,IAAI,QAAc,CAAC,YAAY;AACpC,mBAAK,cAAc,MAAM;AACvB,qBAAK,uBAAuB,KAAK;AACjC,wBAAA;AAAA,cACF;AAAA,YACF,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MAAA;AAGF,aAAO;AAAA,IACT,SAAS,OAAO;AACd,UAAI,iBAAiB,SAAS,MAAM,SAAS,cAAc;AACzD,eAAO;AAAA,MACT;AACA,cAAQ,KAAK,wCAAwC,KAAK;AAC1D,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,oBAA0B;AACxB,QAAI,KAAK,aAAa;AACpB,WAAK,YAAA;AACL,WAAK,cAAc;AAAA,IACrB;AAAA,EACF;AAAA,EAEQ,sBAA+B;AACrC,WAAO,OAAO,cAAc,eAAe,WAAW;AAAA,EACxD;AAAA,EAEA,OAAO,cAAuB;AAC5B,WAAO,OAAO,cAAc,eAAe,WAAW;AAAA,EACxD;AACF;;"}
|
|
@@ -168,8 +168,12 @@ class OfflineExecutor {
|
|
|
168
168
|
);
|
|
169
169
|
this.leaderElection = this.createLeaderElection();
|
|
170
170
|
const isLeader = await this.leaderElection.requestLeadership();
|
|
171
|
+
this.isLeaderState = isLeader;
|
|
171
172
|
span.setAttribute(`isLeader`, isLeader);
|
|
172
173
|
this.setupEventListeners();
|
|
174
|
+
if (this.config.onLeadershipChange) {
|
|
175
|
+
this.config.onLeadershipChange(isLeader);
|
|
176
|
+
}
|
|
173
177
|
if (isLeader) {
|
|
174
178
|
await this.loadAndReplayTransactions();
|
|
175
179
|
}
|
|
@@ -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 { 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 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 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":["OfflineTransactionAPI","action"],"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 { 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":["OfflineTransactionAPI","action"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebLocksLeader.js","sources":["../../../src/coordination/WebLocksLeader.ts"],"sourcesContent":["import { BaseLeaderElection } from './LeaderElection'\n\nexport class WebLocksLeader extends BaseLeaderElection {\n private lockName: string\n private releaseLock: (() => void) | null = null\n\n constructor(lockName = `offline-executor-leader`) {\n super()\n this.lockName = lockName\n }\n\n async requestLeadership(): Promise<boolean> {\n if (!this.isWebLocksSupported()) {\n return false\n }\n\n if (this.isLeaderState) {\n return true\n }\n\n try {\n // First try to acquire the lock with ifAvailable\n const available = await navigator.locks.request(\n this.lockName,\n {\n mode: `exclusive`,\n ifAvailable: true,\n },\n (lock) => {\n return lock !== null\n },\n )\n\n if (!available) {\n return false\n }\n\n // Lock is available, now acquire it for real and hold it\n navigator.locks.request(\n this.lockName,\n {\n mode: `exclusive`,\n },\n async (lock) => {\n if (lock) {\n this.notifyLeadershipChange(true)\n // Hold the lock until released\n return new Promise<void>((resolve) => {\n this.releaseLock = () => {\n this.notifyLeadershipChange(false)\n resolve()\n }\n })\n }\n },\n )\n\n return true\n } catch (error) {\n if (error instanceof Error && error.name === `AbortError`) {\n return false\n }\n console.warn(`Web Locks leadership request failed:`, error)\n return false\n }\n }\n\n releaseLeadership(): void {\n if (this.releaseLock) {\n this.releaseLock()\n this.releaseLock = null\n }\n }\n\n private isWebLocksSupported(): boolean {\n return typeof navigator !== `undefined` && `locks` in navigator\n }\n\n static isSupported(): boolean {\n return typeof navigator !== `undefined` && `locks` in navigator\n }\n}\n"],"names":[],"mappings":";AAEO,MAAM,uBAAuB,mBAAmB;AAAA,EAIrD,YAAY,WAAW,2BAA2B;AAChD,UAAA;AAHF,SAAQ,cAAmC;AAIzC,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,MAAM,oBAAsC;AAC1C,QAAI,CAAC,KAAK,uBAAuB;AAC/B,aAAO;AAAA,IACT;AAEA,QAAI,KAAK,eAAe;AACtB,aAAO;AAAA,IACT;AAEA,QAAI;AAEF,YAAM,YAAY,MAAM,UAAU,MAAM;AAAA,QACtC,KAAK;AAAA,QACL;AAAA,UACE,MAAM;AAAA,UACN,aAAa;AAAA,QAAA;AAAA,QAEf,CAAC,SAAS;AACR,iBAAO,SAAS;AAAA,QAClB;AAAA,MAAA;AAGF,UAAI,CAAC,WAAW;AACd,eAAO;AAAA,MACT;
|
|
1
|
+
{"version":3,"file":"WebLocksLeader.js","sources":["../../../src/coordination/WebLocksLeader.ts"],"sourcesContent":["import { BaseLeaderElection } from './LeaderElection'\n\nexport class WebLocksLeader extends BaseLeaderElection {\n private lockName: string\n private releaseLock: (() => void) | null = null\n\n constructor(lockName = `offline-executor-leader`) {\n super()\n this.lockName = lockName\n }\n\n async requestLeadership(): Promise<boolean> {\n if (!this.isWebLocksSupported()) {\n return false\n }\n\n if (this.isLeaderState) {\n return true\n }\n\n try {\n // First try to acquire the lock with ifAvailable\n const available = await navigator.locks.request(\n this.lockName,\n {\n mode: `exclusive`,\n ifAvailable: true,\n },\n (lock) => {\n return lock !== null\n },\n )\n\n if (!available) {\n return false\n }\n\n // Set state immediately to prevent duplicate notifications\n // when the async lock acquisition calls notifyLeadershipChange(true).\n // The guard in notifyLeadershipChange checks `isLeaderState !== isLeader`,\n // so setting this to true here prevents the callback from firing again.\n this.isLeaderState = true\n\n // Lock is available, now acquire it for real and hold it\n navigator.locks.request(\n this.lockName,\n {\n mode: `exclusive`,\n },\n async (lock) => {\n if (lock) {\n this.notifyLeadershipChange(true)\n // Hold the lock until released\n return new Promise<void>((resolve) => {\n this.releaseLock = () => {\n this.notifyLeadershipChange(false)\n resolve()\n }\n })\n }\n },\n )\n\n return true\n } catch (error) {\n if (error instanceof Error && error.name === `AbortError`) {\n return false\n }\n console.warn(`Web Locks leadership request failed:`, error)\n return false\n }\n }\n\n releaseLeadership(): void {\n if (this.releaseLock) {\n this.releaseLock()\n this.releaseLock = null\n }\n }\n\n private isWebLocksSupported(): boolean {\n return typeof navigator !== `undefined` && `locks` in navigator\n }\n\n static isSupported(): boolean {\n return typeof navigator !== `undefined` && `locks` in navigator\n }\n}\n"],"names":[],"mappings":";AAEO,MAAM,uBAAuB,mBAAmB;AAAA,EAIrD,YAAY,WAAW,2BAA2B;AAChD,UAAA;AAHF,SAAQ,cAAmC;AAIzC,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,MAAM,oBAAsC;AAC1C,QAAI,CAAC,KAAK,uBAAuB;AAC/B,aAAO;AAAA,IACT;AAEA,QAAI,KAAK,eAAe;AACtB,aAAO;AAAA,IACT;AAEA,QAAI;AAEF,YAAM,YAAY,MAAM,UAAU,MAAM;AAAA,QACtC,KAAK;AAAA,QACL;AAAA,UACE,MAAM;AAAA,UACN,aAAa;AAAA,QAAA;AAAA,QAEf,CAAC,SAAS;AACR,iBAAO,SAAS;AAAA,QAClB;AAAA,MAAA;AAGF,UAAI,CAAC,WAAW;AACd,eAAO;AAAA,MACT;AAMA,WAAK,gBAAgB;AAGrB,gBAAU,MAAM;AAAA,QACd,KAAK;AAAA,QACL;AAAA,UACE,MAAM;AAAA,QAAA;AAAA,QAER,OAAO,SAAS;AACd,cAAI,MAAM;AACR,iBAAK,uBAAuB,IAAI;AAEhC,mBAAO,IAAI,QAAc,CAAC,YAAY;AACpC,mBAAK,cAAc,MAAM;AACvB,qBAAK,uBAAuB,KAAK;AACjC,wBAAA;AAAA,cACF;AAAA,YACF,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MAAA;AAGF,aAAO;AAAA,IACT,SAAS,OAAO;AACd,UAAI,iBAAiB,SAAS,MAAM,SAAS,cAAc;AACzD,eAAO;AAAA,MACT;AACA,cAAQ,KAAK,wCAAwC,KAAK;AAC1D,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,oBAA0B;AACxB,QAAI,KAAK,aAAa;AACpB,WAAK,YAAA;AACL,WAAK,cAAc;AAAA,IACrB;AAAA,EACF;AAAA,EAEQ,sBAA+B;AACrC,WAAO,OAAO,cAAc,eAAe,WAAW;AAAA,EACxD;AAAA,EAEA,OAAO,cAAuB;AAC5B,WAAO,OAAO,cAAc,eAAe,WAAW;AAAA,EACxD;AACF;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/offline-transactions",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "Offline-first transaction capabilities for TanStack DB",
|
|
5
5
|
"author": "TanStack",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,11 +40,11 @@
|
|
|
40
40
|
"src"
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@tanstack/db": "0.5.
|
|
43
|
+
"@tanstack/db": "0.5.17"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/node": "^24.6.2",
|
|
47
|
-
"eslint": "^9.39.
|
|
47
|
+
"eslint": "^9.39.2",
|
|
48
48
|
"typescript": "^5.9.2",
|
|
49
49
|
"vitest": "^3.2.4"
|
|
50
50
|
},
|
package/src/OfflineExecutor.ts
CHANGED
|
@@ -264,12 +264,18 @@ export class OfflineExecutor {
|
|
|
264
264
|
|
|
265
265
|
// Request leadership first
|
|
266
266
|
const isLeader = await this.leaderElection.requestLeadership()
|
|
267
|
+
this.isLeaderState = isLeader
|
|
267
268
|
span.setAttribute(`isLeader`, isLeader)
|
|
268
269
|
|
|
269
270
|
// Set up event listeners after leadership is established
|
|
270
271
|
// This prevents the callback from being called multiple times
|
|
271
272
|
this.setupEventListeners()
|
|
272
273
|
|
|
274
|
+
// Notify initial leadership state
|
|
275
|
+
if (this.config.onLeadershipChange) {
|
|
276
|
+
this.config.onLeadershipChange(isLeader)
|
|
277
|
+
}
|
|
278
|
+
|
|
273
279
|
if (isLeader) {
|
|
274
280
|
await this.loadAndReplayTransactions()
|
|
275
281
|
}
|
|
@@ -35,6 +35,12 @@ export class WebLocksLeader extends BaseLeaderElection {
|
|
|
35
35
|
return false
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// Set state immediately to prevent duplicate notifications
|
|
39
|
+
// when the async lock acquisition calls notifyLeadershipChange(true).
|
|
40
|
+
// The guard in notifyLeadershipChange checks `isLeaderState !== isLeader`,
|
|
41
|
+
// so setting this to true here prevents the callback from firing again.
|
|
42
|
+
this.isLeaderState = true
|
|
43
|
+
|
|
38
44
|
// Lock is available, now acquire it for real and hold it
|
|
39
45
|
navigator.locks.request(
|
|
40
46
|
this.lockName,
|