From bb5546f4d45e4ffd06d6349afd3f1288b113d119 Mon Sep 17 00:00:00 2001 From: Toba-bonjour <155877868+Toba-bonjour@users.noreply.github.com> Date: Mon, 22 Dec 2025 19:51:11 +0100 Subject: [PATCH 1/8] feat(offline-transactions): add custom online detector and skip execution when offline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add onlineDetector config option to allow custom online detection - Change onlineDetector type from DefaultOnlineDetector to OnlineDetector interface - Add isOnline() and dispose() methods to OnlineDetector interface - Check online status before executing transactions in TransactionExecutor - Clear scheduler before reloading filtered transactions to ensure consistency - Pass onlineDetector to TransactionExecutor for status checks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../src/OfflineExecutor.ts | 12 ++++++++--- .../src/executor/TransactionExecutor.ts | 20 +++++++++++++++++- packages/offline-transactions/src/types.ts | 5 ++++- pnpm-lock.yaml | 21 ++++++++++++++++++- 4 files changed, 52 insertions(+), 6 deletions(-) diff --git a/packages/offline-transactions/src/OfflineExecutor.ts b/packages/offline-transactions/src/OfflineExecutor.ts index 8ae8bd375..8e5886c61 100644 --- a/packages/offline-transactions/src/OfflineExecutor.ts +++ b/packages/offline-transactions/src/OfflineExecutor.ts @@ -30,6 +30,7 @@ import type { OfflineConfig, OfflineMode, OfflineTransaction, + OnlineDetector, StorageAdapter, StorageDiagnostic, } from './types' @@ -44,7 +45,7 @@ export class OfflineExecutor { private scheduler: KeyScheduler private executor: TransactionExecutor | null private leaderElection: LeaderElection | null - private onlineDetector: DefaultOnlineDetector + private onlineDetector: OnlineDetector private isLeaderState = false private unsubscribeOnline: (() => void) | null = null private unsubscribeLeadership: (() => void) | null = null @@ -71,7 +72,11 @@ export class OfflineExecutor { constructor(config: OfflineConfig) { this.config = config this.scheduler = new KeyScheduler() - this.onlineDetector = new DefaultOnlineDetector() + + // Initialize onlineDetector based on config + // undefined = use DefaultOnlineDetector (default) + // custom = user-provided detector + this.onlineDetector = config.onlineDetector ?? new DefaultOnlineDetector() // Initialize as pending - will be set by async initialization this.storage = null @@ -259,6 +264,7 @@ export class OfflineExecutor { this.outbox, this.config, this, + this.onlineDetector, ) this.leaderElection = this.createLeaderElection() @@ -485,7 +491,7 @@ export class OfflineExecutor { return this.executor.getRunningCount() } - getOnlineDetector(): DefaultOnlineDetector { + getOnlineDetector(): OnlineDetector { return this.onlineDetector } diff --git a/packages/offline-transactions/src/executor/TransactionExecutor.ts b/packages/offline-transactions/src/executor/TransactionExecutor.ts index 441a87d88..38b74fa6a 100644 --- a/packages/offline-transactions/src/executor/TransactionExecutor.ts +++ b/packages/offline-transactions/src/executor/TransactionExecutor.ts @@ -3,7 +3,11 @@ import { NonRetriableError } from '../types' import { withNestedSpan } from '../telemetry/tracer' import type { KeyScheduler } from './KeyScheduler' import type { OutboxManager } from '../outbox/OutboxManager' -import type { OfflineConfig, OfflineTransaction } from '../types' +import type { + OfflineConfig, + OfflineTransaction, + OnlineDetector, +} from '../types' const HANDLED_EXECUTION_ERROR = Symbol(`HandledExecutionError`) @@ -16,18 +20,21 @@ export class TransactionExecutor { private executionPromise: Promise | null = null private offlineExecutor: any // Reference to OfflineExecutor for signaling private retryTimer: ReturnType | null = null + private onlineDetector: OnlineDetector constructor( scheduler: KeyScheduler, outbox: OutboxManager, config: OfflineConfig, offlineExecutor: any, + onlineDetector: OnlineDetector, ) { this.scheduler = scheduler this.outbox = outbox this.config = config this.retryPolicy = new DefaultRetryPolicy(10, config.jitter ?? true) this.offlineExecutor = offlineExecutor + this.onlineDetector = onlineDetector } async execute(transaction: OfflineTransaction): Promise { @@ -54,6 +61,12 @@ export class TransactionExecutor { private async runExecution(): Promise { const maxConcurrency = this.config.maxConcurrency ?? 3 + // Check online status before executing transactions + if (!this.onlineDetector.isOnline()) { + console.log('Network offline, pausing execution') + return + } + while (this.scheduler.getPendingCount() > 0) { const batch = this.scheduler.getNextBatch(maxConcurrency) @@ -221,8 +234,12 @@ export class TransactionExecutor { if (this.config.beforeRetry) { filteredTransactions = this.config.beforeRetry(transactions) + console.log('loadPendingTransactions, transactions has been filtered', {filteredTransactions}) } + // Clear scheduler before reloading to ensure filtered-out transactions are removed + this.scheduler.clear() + for (const transaction of filteredTransactions) { this.scheduler.schedule(transaction) } @@ -238,6 +255,7 @@ export class TransactionExecutor { ) if (removedTransactions.length > 0) { + console.log('removedTransactions', removedTransactions) await this.outbox.removeMany(removedTransactions.map((tx) => tx.id)) } } diff --git a/packages/offline-transactions/src/types.ts b/packages/offline-transactions/src/types.ts index 8cf18cc88..670955c48 100644 --- a/packages/offline-transactions/src/types.ts +++ b/packages/offline-transactions/src/types.ts @@ -88,7 +88,7 @@ export interface StorageDiagnostic { } export interface OfflineConfig { - // eslint-disable-next-line @typescript-eslint/no-explicit-any + collections: Record> mutationFns: Record storage?: StorageAdapter @@ -101,6 +101,7 @@ export interface OfflineConfig { onLeadershipChange?: (isLeader: boolean) => void onStorageFailure?: (diagnostic: StorageDiagnostic) => void leaderElection?: LeaderElection + onlineDetector?: OnlineDetector } export interface StorageAdapter { @@ -126,6 +127,8 @@ export interface LeaderElection { export interface OnlineDetector { subscribe: (callback: () => void) => () => void notifyOnline: () => void + isOnline: () => boolean + dispose: () => void } export interface CreateOfflineTransactionOptions { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b06f5c89..e8dae7f6e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -188,7 +188,7 @@ importers: dependencies: '@tanstack/offline-transactions': specifier: ^1.0.5 - version: link:../../../packages/offline-transactions + version: link:../../../packages/offline-transactions/package 2 '@tanstack/query-db-collection': specifier: ^1.0.11 version: link:../../../packages/query-db-collection @@ -816,6 +816,25 @@ importers: specifier: ^3.2.4 version: 3.2.4(@types/debug@4.1.12)(@types/node@24.7.0)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1) + packages/offline-transactions/package 2: + dependencies: + '@tanstack/db': + specifier: 0.5.15 + version: link:../../db + devDependencies: + '@types/node': + specifier: ^24.6.2 + version: 24.7.0 + eslint: + specifier: ^9.39.1 + version: 9.39.1(jiti@2.6.1) + typescript: + specifier: ^5.9.2 + version: 5.9.3 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.7.0)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1) + packages/powersync-db-collection: dependencies: '@standard-schema/spec': From 3988b15e22a27c61f1cce15f21cec076d69ab154 Mon Sep 17 00:00:00 2001 From: Toba-bonjour <155877868+Toba-bonjour@users.noreply.github.com> Date: Mon, 22 Dec 2025 22:22:57 +0100 Subject: [PATCH 2/8] update documentation --- packages/offline-transactions/README.md | 62 +++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/packages/offline-transactions/README.md b/packages/offline-transactions/README.md index 334125a60..cd0508ed9 100644 --- a/packages/offline-transactions/README.md +++ b/packages/offline-transactions/README.md @@ -5,6 +5,7 @@ Offline-first transaction capabilities for TanStack DB that provides durable per ## Features - **Outbox Pattern**: Persist mutations before dispatch for zero data loss +- **Offline Detection**: Skip retries when offline, auto-resume when connectivity restored - **Automatic Retry**: Exponential backoff with jitter for failed transactions - **Multi-tab Coordination**: Leader election ensures safe storage access - **FIFO Sequential Processing**: Transactions execute one at a time in creation order @@ -98,9 +99,21 @@ interface OfflineConfig { beforeRetry?: (transactions: OfflineTransaction[]) => OfflineTransaction[] onUnknownMutationFn?: (name: string, tx: OfflineTransaction) => void onLeadershipChange?: (isLeader: boolean) => void + onlineDetector?: OnlineDetector +} + +interface OnlineDetector { + subscribe: (callback: () => void) => () => void + notifyOnline: () => void + isOnline: () => boolean + dispose: () => void } ``` +### onlineDetector +By default, `onlineDetector` is `undefined` and will use the built-in `DefaultOnlineDetector` which uses the browser's `navigator.onLine` API. You can provide your own implementation for more reliability. + + ### OfflineExecutor #### Properties @@ -137,6 +150,55 @@ const mutationFn = async ({ transaction }) => { ## Advanced Usage +### Custom Online Detector + +By default, the executor uses the browser's `navigator.onLine` API to detect connectivity. You can provide a custom detector for more sophisticated detection logic: + +```typescript +class CustomOnlineDetector implements OnlineDetector { + private listeners = new Set<() => void>() + private online = true + + constructor() { + // Poll your API endpoint to check connectivity + setInterval(async () => { + try { + await fetch('/api/health', { method: 'HEAD' }) + const wasOffline = !this.online + this.online = true + if (wasOffline) { + this.notifyOnline() + } + } catch { + this.online = false + } + }, 5000) + } + + isOnline(): boolean { + return this.online + } + + subscribe(callback: () => void): () => void { + this.listeners.add(callback) + return () => this.listeners.delete(callback) + } + + notifyOnline(): void { + this.listeners.forEach((cb) => cb()) + } + + dispose(): void { + this.listeners.clear() + } +} + +const executor = startOfflineExecutor({ + onlineDetector: new CustomOnlineDetector(), + // ... other config +}) +``` + ### Custom Storage Adapter ```typescript From f4397da90de67d3b437f02bb0a5f58b34c3cdd1a Mon Sep 17 00:00:00 2001 From: Toba-bonjour <155877868+Toba-bonjour@users.noreply.github.com> Date: Mon, 22 Dec 2025 22:37:34 +0100 Subject: [PATCH 3/8] add changeset --- .changeset/dirty-papayas-attend.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dirty-papayas-attend.md diff --git a/.changeset/dirty-papayas-attend.md b/.changeset/dirty-papayas-attend.md new file mode 100644 index 000000000..f46948972 --- /dev/null +++ b/.changeset/dirty-papayas-attend.md @@ -0,0 +1,5 @@ +--- +'@tanstack/offline-transactions': minor +--- + +Enrich the OfflineConfig interface with an onlineDetector option and Check online status before executing transactions. From f6456ef29806e9607655b59c824e57a7df2b77ec Mon Sep 17 00:00:00 2001 From: Toba-bonjour <155877868+Toba-bonjour@users.noreply.github.com> Date: Mon, 22 Dec 2025 23:02:02 +0100 Subject: [PATCH 4/8] remove unnecessary files --- .../src/executor/TransactionExecutor.ts | 3 --- pnpm-lock.yaml | 19 ------------------- 2 files changed, 22 deletions(-) diff --git a/packages/offline-transactions/src/executor/TransactionExecutor.ts b/packages/offline-transactions/src/executor/TransactionExecutor.ts index 38b74fa6a..f14596d6c 100644 --- a/packages/offline-transactions/src/executor/TransactionExecutor.ts +++ b/packages/offline-transactions/src/executor/TransactionExecutor.ts @@ -237,8 +237,6 @@ export class TransactionExecutor { console.log('loadPendingTransactions, transactions has been filtered', {filteredTransactions}) } - // Clear scheduler before reloading to ensure filtered-out transactions are removed - this.scheduler.clear() for (const transaction of filteredTransactions) { this.scheduler.schedule(transaction) @@ -255,7 +253,6 @@ export class TransactionExecutor { ) if (removedTransactions.length > 0) { - console.log('removedTransactions', removedTransactions) await this.outbox.removeMany(removedTransactions.map((tx) => tx.id)) } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e8dae7f6e..cb2054486 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -816,25 +816,6 @@ importers: specifier: ^3.2.4 version: 3.2.4(@types/debug@4.1.12)(@types/node@24.7.0)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1) - packages/offline-transactions/package 2: - dependencies: - '@tanstack/db': - specifier: 0.5.15 - version: link:../../db - devDependencies: - '@types/node': - specifier: ^24.6.2 - version: 24.7.0 - eslint: - specifier: ^9.39.1 - version: 9.39.1(jiti@2.6.1) - typescript: - specifier: ^5.9.2 - version: 5.9.3 - vitest: - specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.7.0)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1) - packages/powersync-db-collection: dependencies: '@standard-schema/spec': From b0b41b2ff8f71234187f0817bcd78643fc6bc3d2 Mon Sep 17 00:00:00 2001 From: Toba-bonjour <155877868+Toba-bonjour@users.noreply.github.com> Date: Mon, 22 Dec 2025 23:19:22 +0100 Subject: [PATCH 5/8] chore: remove debug console.log statements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../offline-transactions/src/executor/TransactionExecutor.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/offline-transactions/src/executor/TransactionExecutor.ts b/packages/offline-transactions/src/executor/TransactionExecutor.ts index f14596d6c..540670cc6 100644 --- a/packages/offline-transactions/src/executor/TransactionExecutor.ts +++ b/packages/offline-transactions/src/executor/TransactionExecutor.ts @@ -63,7 +63,6 @@ export class TransactionExecutor { // Check online status before executing transactions if (!this.onlineDetector.isOnline()) { - console.log('Network offline, pausing execution') return } @@ -234,7 +233,6 @@ export class TransactionExecutor { if (this.config.beforeRetry) { filteredTransactions = this.config.beforeRetry(transactions) - console.log('loadPendingTransactions, transactions has been filtered', {filteredTransactions}) } From d68bebe7e3e195f402eb3adcc596f487c9168c7c Mon Sep 17 00:00:00 2001 From: Toba-bonjour <155877868+Toba-bonjour@users.noreply.github.com> Date: Mon, 22 Dec 2025 23:30:57 +0100 Subject: [PATCH 6/8] chore: update pnpm-lock.yaml after rebase on upstream/main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- pnpm-lock.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb2054486..8b06f5c89 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -188,7 +188,7 @@ importers: dependencies: '@tanstack/offline-transactions': specifier: ^1.0.5 - version: link:../../../packages/offline-transactions/package 2 + version: link:../../../packages/offline-transactions '@tanstack/query-db-collection': specifier: ^1.0.11 version: link:../../../packages/query-db-collection From 338a13f7cf21a5a0c90b43ea0848be8e5d41f7b3 Mon Sep 17 00:00:00 2001 From: Toba-bonjour <155877868+Toba-bonjour@users.noreply.github.com> Date: Tue, 23 Dec 2025 14:51:39 +0100 Subject: [PATCH 7/8] add tests --- .../offline-transactions/tests/harness.ts | 32 ++++ .../tests/offline-e2e.test.ts | 152 +++++++++++++++++- 2 files changed, 183 insertions(+), 1 deletion(-) diff --git a/packages/offline-transactions/tests/harness.ts b/packages/offline-transactions/tests/harness.ts index 7f8dc82ad..50670cf9f 100644 --- a/packages/offline-transactions/tests/harness.ts +++ b/packages/offline-transactions/tests/harness.ts @@ -5,6 +5,7 @@ import type { LeaderElection, OfflineConfig, OfflineMutationFnParams, + OnlineDetector, StorageAdapter, } from '../src/types' @@ -92,6 +93,36 @@ class FakeLeaderElection implements LeaderElection { } } +export class FakeOnlineDetector implements OnlineDetector { + private listeners = new Set<() => void>() + online = true + + isOnline(): boolean { + return this.online + } + + subscribe(callback: () => void): () => void { + this.listeners.add(callback) + return () => { + this.listeners.delete(callback) + } + } + + notifyOnline(): void { + for (const listener of this.listeners) { + try { + listener() + } catch (error) { + console.warn(`FakeOnlineDetector listener error:`, error) + } + } + } + + dispose(): void { + this.listeners.clear() + } +} + type TestMutationFn = ( params: OfflineMutationFnParams & { attempt: number }, ) => Promise @@ -243,6 +274,7 @@ export function createTestOfflineEnvironment( onUnknownMutationFn: options.config?.onUnknownMutationFn, onLeadershipChange: options.config?.onLeadershipChange, leaderElection: options.config?.leaderElection ?? leader, + onlineDetector: options.config?.onlineDetector, } const executor = startOfflineExecutor(config) diff --git a/packages/offline-transactions/tests/offline-e2e.test.ts b/packages/offline-transactions/tests/offline-e2e.test.ts index b4f9442df..c348fa297 100644 --- a/packages/offline-transactions/tests/offline-e2e.test.ts +++ b/packages/offline-transactions/tests/offline-e2e.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it } from 'vitest' import { NonRetriableError } from '../src/types' -import { FakeStorageAdapter, createTestOfflineEnvironment } from './harness' +import { + FakeOnlineDetector, + FakeStorageAdapter, + createTestOfflineEnvironment, +} from './harness' import type { TestItem } from './harness' import type { OfflineMutationFnParams } from '../src/types' import type { PendingMutation } from '@tanstack/db' @@ -428,6 +432,7 @@ describe(`offline executor end-to-end`, () => { // Give the executor a moment to react to leadership change await flushMicrotasks() + // Should not be offline enabled when not leader expect(env.executor.isOfflineEnabled).toBe(false) @@ -466,4 +471,149 @@ describe(`offline executor end-to-end`, () => { env.executor.dispose() }) + + it(`accepts a custom OnlineDetector via config`, async () => { + const customDetector = new FakeOnlineDetector() + + const env = createTestOfflineEnvironment({ + config: { + onlineDetector: customDetector, + }, + }) + + await env.waitForLeader() + + + // Verify the executor uses the custom detector + expect(env.executor.getOnlineDetector()).toBe(customDetector) + + env.executor.dispose() + }) + + it(`uses custom OnlineDetector instead of default`, async () => { + const customDetector = new FakeOnlineDetector() + + // Set detector to offline initially + customDetector.online = false + + const env = createTestOfflineEnvironment({ + config: { + onlineDetector: customDetector, + }, + }) + + await env.waitForLeader() + + // Create a transaction while offline + const offlineTx = env.executor.createOfflineTransaction({ + mutationFnName: env.mutationFnName, + autoCommit: false, + }) + + offlineTx.mutate(() => { + env.collection.insert({ + id: `custom-detector-item`, + value: `test`, + completed: false, + updatedAt: new Date(), + }) + }) + + const commitPromise = offlineTx.commit() + await flushMicrotasks() + + // Transaction should not execute because custom detector reports offline + expect(env.mutationCalls.length).toBe(0) + + // Verify transaction is in outbox + let outboxEntries = await env.executor.peekOutbox() + expect(outboxEntries.length).toBe(1) + + // Set detector back online and notify + customDetector.online = true + customDetector.notifyOnline() + + // Wait for transaction to execute + await waitUntil(() => env.mutationCalls.length >= 1) + + await commitPromise + + // Verify transaction completed + outboxEntries = await env.executor.peekOutbox() + expect(outboxEntries).toEqual([]) + expect(env.serverState.get(`custom-detector-item`)?.value).toBe(`test`) + + env.executor.dispose() + }) + + it(`queues transaction created offline, then executes when going online`, async () => { + const customDetector = new FakeOnlineDetector() + + // Start offline + customDetector.online = false + + const env = createTestOfflineEnvironment({ + config: { + onlineDetector: customDetector, + }, + }) + + await env.waitForLeader() + + // Create and commit transaction while offline + const offlineTx = env.executor.createOfflineTransaction({ + mutationFnName: env.mutationFnName, + autoCommit: false, + }) + + const now = new Date() + offlineTx.mutate(() => { + env.collection.insert({ + id: `offline-queued`, + value: `will-sync-later`, + completed: false, + updatedAt: now, + }) + }) + + const commitPromise = offlineTx.commit() + await flushMicrotasks() + + // Transaction should not have executed yet (offline) + expect(env.mutationCalls.length).toBe(0) + + // Transaction should be in outbox (queued) + let outboxEntries = await env.executor.peekOutbox() + expect(outboxEntries.length).toBe(1) + expect(outboxEntries[0].id).toBe(offlineTx.id) + + // Local state should have the optimistic update + expect(env.collection.get(`offline-queued`)?.value).toBe(`will-sync-later`) + + // Server should not have it yet + expect(env.serverState.get(`offline-queued`)).toBeUndefined() + + // Go back online + customDetector.online = true + customDetector.notifyOnline() + + // Wait for the transaction to execute + await waitUntil(() => env.mutationCalls.length >= 1) + + // Commit promise should resolve + await commitPromise + + // Verify transaction executed successfully + expect(env.mutationCalls.length).toBe(1) + + // Transaction should be removed from outbox + outboxEntries = await env.executor.peekOutbox() + expect(outboxEntries).toEqual([]) + + // Both local and server should have the data + expect(env.collection.get(`offline-queued`)?.value).toBe(`will-sync-later`) + expect(env.serverState.get(`offline-queued`)?.value).toBe(`will-sync-later`) + + env.executor.dispose() + }) }) From e5b5066612154d59add71bfc9d494677f22e8cc0 Mon Sep 17 00:00:00 2001 From: Toba-bonjour <155877868+Toba-bonjour@users.noreply.github.com> Date: Wed, 24 Dec 2025 11:49:59 +0100 Subject: [PATCH 8/8] Clarify doc. Add console.info to notifyOnline method when offline --- packages/offline-transactions/README.md | 26 ++++++++++++++++--- .../src/connectivity/OnlineDetector.ts | 4 +++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/packages/offline-transactions/README.md b/packages/offline-transactions/README.md index cd0508ed9..4238160d4 100644 --- a/packages/offline-transactions/README.md +++ b/packages/offline-transactions/README.md @@ -110,8 +110,26 @@ interface OnlineDetector { } ``` -### onlineDetector -By default, `onlineDetector` is `undefined` and will use the built-in `DefaultOnlineDetector` which uses the browser's `navigator.onLine` API. You can provide your own implementation for more reliability. +### onlineDetector + +By default, `onlineDetector` is `undefined` and the system will use the built-in `DefaultOnlineDetector`. + +**How it works:** +- Provides an `isOnline()` method to check connectivity status before executing transactions + +**Transactions are skipped when offline** +- Avoid unnecessary retry attempts +- Allows subscribers to be notified when connectivity is restored, triggering pending transaction execution + +**DefaultOnlineDetector behavior:** +- Uses the browser's `navigator.onLine` API to detect online/offline state +- Automatically triggers transaction execution on these events: + - `online` event (browser detects network connection) + - `visibilitychange` event (when tab becomes visible) + +**Manual trigger:** +- `notifyOnline()` method can be used to manually trigger transaction execution +- Only succeeds if `isOnline()` returns `true` ### OfflineExecutor @@ -126,7 +144,7 @@ By default, `onlineDetector` is `undefined` and will use the built-in `DefaultOn - `waitForTransactionCompletion(id)` - Wait for a specific transaction to complete - `removeFromOutbox(id)` - Manually remove transaction from outbox - `peekOutbox()` - View all pending transactions -- `notifyOnline()` - Manually trigger retry execution +- `notifyOnline()` - Manually trigger transaction execution (only succeeds if online) - `dispose()` - Clean up resources ### Error Handling @@ -172,7 +190,7 @@ class CustomOnlineDetector implements OnlineDetector { } catch { this.online = false } - }, 5000) + }, 60000) } isOnline(): boolean { diff --git a/packages/offline-transactions/src/connectivity/OnlineDetector.ts b/packages/offline-transactions/src/connectivity/OnlineDetector.ts index 9ebd587bf..14a1deb50 100644 --- a/packages/offline-transactions/src/connectivity/OnlineDetector.ts +++ b/packages/offline-transactions/src/connectivity/OnlineDetector.ts @@ -70,6 +70,10 @@ export class DefaultOnlineDetector implements OnlineDetector { } notifyOnline(): void { + if (!this.isOnline()) { + console.info('notifyOnline called while offline, skipping notification') + return + } this.notifyListeners() }