@tanstack/offline-transactions 1.0.54 → 1.0.58

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.
Files changed (64) hide show
  1. package/README.md +137 -23
  2. package/dist/cjs/OfflineExecutor.cjs +28 -11
  3. package/dist/cjs/OfflineExecutor.cjs.map +1 -1
  4. package/dist/cjs/OfflineExecutor.d.cts +1 -0
  5. package/dist/cjs/api/OfflineTransaction.cjs +4 -1
  6. package/dist/cjs/api/OfflineTransaction.cjs.map +1 -1
  7. package/dist/cjs/connectivity/OnlineDetector.cjs +1 -1
  8. package/dist/cjs/connectivity/OnlineDetector.cjs.map +1 -1
  9. package/dist/cjs/connectivity/ReactNativeOnlineDetector.cjs +1 -7
  10. package/dist/cjs/connectivity/ReactNativeOnlineDetector.cjs.map +1 -1
  11. package/dist/cjs/executor/KeyScheduler.cjs +31 -26
  12. package/dist/cjs/executor/KeyScheduler.cjs.map +1 -1
  13. package/dist/cjs/executor/KeyScheduler.d.cts +5 -3
  14. package/dist/cjs/executor/TransactionExecutor.cjs +72 -35
  15. package/dist/cjs/executor/TransactionExecutor.cjs.map +1 -1
  16. package/dist/cjs/executor/TransactionExecutor.d.cts +3 -2
  17. package/dist/cjs/outbox/OutboxManager.cjs +66 -24
  18. package/dist/cjs/outbox/OutboxManager.cjs.map +1 -1
  19. package/dist/cjs/outbox/OutboxManager.d.cts +6 -0
  20. package/dist/cjs/outbox/TransactionSerializer.cjs +159 -20
  21. package/dist/cjs/outbox/TransactionSerializer.cjs.map +1 -1
  22. package/dist/cjs/outbox/TransactionSerializer.d.cts +2 -0
  23. package/dist/cjs/storage/IndexedDBAdapter.cjs +16 -12
  24. package/dist/cjs/storage/IndexedDBAdapter.cjs.map +1 -1
  25. package/dist/cjs/storage/IndexedDBAdapter.d.cts +1 -0
  26. package/dist/cjs/types.cjs.map +1 -1
  27. package/dist/cjs/types.d.cts +3 -0
  28. package/dist/esm/OfflineExecutor.d.ts +1 -0
  29. package/dist/esm/OfflineExecutor.js +28 -11
  30. package/dist/esm/OfflineExecutor.js.map +1 -1
  31. package/dist/esm/api/OfflineTransaction.js +4 -1
  32. package/dist/esm/api/OfflineTransaction.js.map +1 -1
  33. package/dist/esm/connectivity/OnlineDetector.js +1 -1
  34. package/dist/esm/connectivity/OnlineDetector.js.map +1 -1
  35. package/dist/esm/connectivity/ReactNativeOnlineDetector.js +1 -7
  36. package/dist/esm/connectivity/ReactNativeOnlineDetector.js.map +1 -1
  37. package/dist/esm/executor/KeyScheduler.d.ts +5 -3
  38. package/dist/esm/executor/KeyScheduler.js +31 -26
  39. package/dist/esm/executor/KeyScheduler.js.map +1 -1
  40. package/dist/esm/executor/TransactionExecutor.d.ts +3 -2
  41. package/dist/esm/executor/TransactionExecutor.js +72 -35
  42. package/dist/esm/executor/TransactionExecutor.js.map +1 -1
  43. package/dist/esm/outbox/OutboxManager.d.ts +6 -0
  44. package/dist/esm/outbox/OutboxManager.js +67 -25
  45. package/dist/esm/outbox/OutboxManager.js.map +1 -1
  46. package/dist/esm/outbox/TransactionSerializer.d.ts +2 -0
  47. package/dist/esm/outbox/TransactionSerializer.js +159 -20
  48. package/dist/esm/outbox/TransactionSerializer.js.map +1 -1
  49. package/dist/esm/storage/IndexedDBAdapter.d.ts +1 -0
  50. package/dist/esm/storage/IndexedDBAdapter.js +16 -12
  51. package/dist/esm/storage/IndexedDBAdapter.js.map +1 -1
  52. package/dist/esm/types.d.ts +3 -0
  53. package/dist/esm/types.js.map +1 -1
  54. package/package.json +2 -2
  55. package/src/OfflineExecutor.ts +34 -15
  56. package/src/api/OfflineTransaction.ts +7 -2
  57. package/src/connectivity/OnlineDetector.ts +6 -1
  58. package/src/connectivity/ReactNativeOnlineDetector.ts +1 -11
  59. package/src/executor/KeyScheduler.ts +39 -28
  60. package/src/executor/TransactionExecutor.ts +88 -46
  61. package/src/outbox/OutboxManager.ts +82 -28
  62. package/src/outbox/TransactionSerializer.ts +236 -22
  63. package/src/storage/IndexedDBAdapter.ts +23 -12
  64. package/src/types.ts +3 -0
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # @tanstack/offline-transactions
2
2
 
3
- Offline-first transaction capabilities for TanStack DB that provides durable persistence of mutations with automatic retry when connectivity is restored.
3
+ This package gives the leader a durable outbox for pending TanStack DB mutations. It retries stored mutations when the server is available. Read the [Offline Transactions guide](../../docs/guides/offline-transactions.md) for setup and lifecycle behavior.
4
4
 
5
5
  ## Features
6
6
 
7
- - **Outbox Pattern**: Persist mutations before dispatch for zero data loss
7
+ - **Outbox**: The leader stores mutations before sending them when `isOfflineEnabled` is true
8
8
  - **Automatic Retry**: Configurable retry behavior with exponential backoff + jitter by default
9
- - **Multi-tab Coordination**: Leader election ensures safe storage access
9
+ - **Multi-tab Coordination**: Leader election chooses one tab to process the outbox
10
10
  - **FIFO Sequential Processing**: Transactions execute one at a time in creation order
11
11
  - **Flexible Storage**: IndexedDB with localStorage fallback
12
12
  - **Type Safe**: Full TypeScript support with TanStack DB integration
@@ -25,18 +25,18 @@ npm install @tanstack/offline-transactions
25
25
  npm install @tanstack/offline-transactions @react-native-community/netinfo
26
26
  ```
27
27
 
28
- The React Native implementation requires the `@react-native-community/netinfo` peer dependency for network connectivity detection.
28
+ The React Native entry point uses `@react-native-community/netinfo` for connectivity detection. Supply a `StorageAdapter` for the outbox. The package does not include an AsyncStorage adapter.
29
29
 
30
30
  ## Platform Support
31
31
 
32
32
  This package provides platform-specific implementations for web and React Native environments:
33
33
 
34
- - **Web**: Uses browser APIs (`window.online/offline` events, `document.visibilitychange`)
34
+ - **Web**: Uses browser APIs (`window.online` and `document.visibilitychange` events). Visible tabs allow sync attempts even when `navigator.onLine` is false; hidden tabs follow that hint. Request failures still use the configured retry policy. Visibility is local to each tab and does not transfer leadership from a hidden tab.
35
35
  - **React Native**: Uses React Native primitives (`@react-native-community/netinfo` for network status, `AppState` for foreground/background detection)
36
36
 
37
37
  ## Quick Start
38
38
 
39
- Using offline transactions on web and React Native/Expo is identical except for the import. Choose the appropriate import based on your target platform:
39
+ Use the entry point for your platform. React Native also needs a storage adapter, as shown in the [guide](../../docs/guides/offline-transactions.md#react-native-and-expo).
40
40
 
41
41
  **Web:**
42
42
 
@@ -50,7 +50,7 @@ import { startOfflineExecutor } from '@tanstack/offline-transactions'
50
50
  import { startOfflineExecutor } from '@tanstack/offline-transactions/react-native'
51
51
  ```
52
52
 
53
- **Usage (same for both platforms):**
53
+ **Web usage:**
54
54
 
55
55
  ```typescript
56
56
  // Setup offline executor
@@ -68,13 +68,15 @@ const offline = startOfflineExecutor({
68
68
  },
69
69
  })
70
70
 
71
+ await offline.waitForInit()
72
+
71
73
  // Create offline transactions
72
74
  const offlineTx = offline.createOfflineTransaction({
73
75
  mutationFnName: 'syncTodos',
74
76
  autoCommit: false,
75
77
  })
76
78
 
77
- offlineTx.mutate(() => {
79
+ const transaction = offlineTx.mutate(() => {
78
80
  todoCollection.insert({
79
81
  id: crypto.randomUUID(),
80
82
  text: 'Buy milk',
@@ -82,20 +84,25 @@ offlineTx.mutate(() => {
82
84
  })
83
85
  })
84
86
 
85
- // Execute with automatic offline support
86
- await offlineTx.commit()
87
+ // Commit can remain pending while offline. Observe final failure.
88
+ void offlineTx.commit().catch((error) => console.error(error))
89
+ void transaction.isPersisted.promise.catch((error) => console.error(error))
87
90
  ```
88
91
 
92
+ On React Native, pass a custom `storage` adapter to `startOfflineExecutor`.
93
+
89
94
  ## Core Concepts
90
95
 
91
- ### Outbox-First Persistence
96
+ ### Durable Outbox
92
97
 
93
- Mutations are persisted to a durable outbox before being applied, ensuring zero data loss during offline periods:
98
+ When `isOfflineEnabled` is true, the executor records a mutation before it sends the mutation to the server. The optimistic change appears before the outbox write settles:
94
99
 
95
- 1. Mutation is persisted to IndexedDB/localStorage
96
- 2. Optimistic update is applied locally
97
- 3. When online, mutation is sent to server
98
- 4. On success, mutation is removed from outbox
100
+ 1. The Collection applies an optimistic mutation.
101
+ 2. The leader writes the transaction to the outbox.
102
+ 3. When online, the executor calls the named mutation function.
103
+ 4. After a successful call, the executor attempts to remove the outbox entry.
104
+
105
+ An optimistic change does not prove that the outbox write succeeded. Handle transaction failures, and use an idempotency key on the server because an attempt can run more than once.
99
106
 
100
107
  ### Multi-tab Coordination
101
108
 
@@ -107,11 +114,11 @@ Only one tab acts as the "leader" to safely manage the outbox:
107
114
 
108
115
  ### FIFO Sequential Processing
109
116
 
110
- Transactions are processed one at a time in the order they were created:
117
+ The executor processes one transaction at a time, in creation order:
111
118
 
112
119
  - **Sequential execution**: All transactions execute in FIFO order
113
120
  - **Dependency safety**: Avoids conflicts between transactions that may reference each other
114
- - **Predictable behavior**: Transactions complete in the exact order they were created
121
+ - **Predictable behavior**: Transactions complete in creation order
115
122
 
116
123
  ## API Reference
117
124
 
@@ -200,6 +207,108 @@ tx.mutate(() => {
200
207
  await tx.commit()
201
208
  ```
202
209
 
210
+ ## Tracking Submission Status
211
+
212
+ The `Transaction` returned by `mutate()` exposes local transaction state. Its
213
+ `state` starts as `pending`, becomes `persisting` during commit, and settles as
214
+ `completed` or `failed`. A rollback can move it to `failed` before or during
215
+ persistence. `isPersisted.promise` settles at the same success or failure
216
+ boundary.
217
+
218
+ On the offline-execution path, successful settlement means the configured
219
+ `mutationFn` returned. The executor also attempts to remove the durable outbox
220
+ entry. It means the server confirmed or exposed the write only when that
221
+ `mutationFn` explicitly waits for the provider's acknowledgement, read-back, or
222
+ sync observation before returning.
223
+
224
+ ```typescript
225
+ const offlineTx = offline.createOfflineTransaction({
226
+ mutationFnName: 'syncTodos',
227
+ autoCommit: false,
228
+ })
229
+
230
+ const tx = offlineTx.mutate(() => {
231
+ todoCollection.insert({ id: '1', text: 'Buy milk', completed: false })
232
+ })
233
+
234
+ console.log(tx.state) // 'pending'
235
+
236
+ try {
237
+ await Promise.all([offlineTx.commit(), tx.isPersisted.promise])
238
+ console.log(tx.state) // 'completed'
239
+ } catch (error) {
240
+ showSubmissionError(error)
241
+ }
242
+ ```
243
+
244
+ ### Tracking Every Pending Transaction for an Item
245
+
246
+ An item can have more than one transaction in flight. Track transaction
247
+ identities rather than storing one replaceable boolean or deleting an
248
+ item-keyed entry unconditionally:
249
+
250
+ ```typescript
251
+ import type { Transaction } from '@tanstack/db'
252
+
253
+ const pendingByItem = new Map<string, Set<Transaction>>()
254
+
255
+ function trackPending(itemId: string, tx: Transaction) {
256
+ const pending = pendingByItem.get(itemId) ?? new Set<Transaction>()
257
+ pending.add(tx)
258
+ pendingByItem.set(itemId, pending)
259
+
260
+ const removeThisTransaction = () => {
261
+ pending.delete(tx)
262
+ if (pending.size === 0 && pendingByItem.get(itemId) === pending) {
263
+ pendingByItem.delete(itemId)
264
+ }
265
+ }
266
+
267
+ // Handle fulfillment and rejection so cleanup does not create another
268
+ // rejected Promise chain.
269
+ void tx.isPersisted.promise.then(removeThisTransaction, removeThisTransaction)
270
+ }
271
+
272
+ function isPending(itemId: string): boolean {
273
+ return (pendingByItem.get(itemId)?.size ?? 0) > 0
274
+ }
275
+ ```
276
+
277
+ Using only `pendingItems.delete(itemId)` in an older transaction's completion
278
+ handler is unsafe because it can remove a newer transaction's status. A map that
279
+ represents only "the latest submission" must compare the current entry with the finishing transaction before cleanup:
280
+
281
+ ```typescript
282
+ if (latestByItem.get(itemId) === tx) {
283
+ latestByItem.delete(itemId)
284
+ }
285
+ ```
286
+
287
+ That latest-only map can still be empty while an older transaction is pending
288
+ if the newer transaction settles first. Use a set as above when the UI must
289
+ answer whether _any_ submission remains pending.
290
+
291
+ ### Inspecting Offline Work
292
+
293
+ The executor exposes point-in-time scheduler counts and the durable outbox:
294
+
295
+ ```typescript
296
+ // Scheduled entries. The pending count can include the currently running entry.
297
+ const pendingCount = offline.getPendingCount()
298
+ const runningCount = offline.getRunningCount()
299
+
300
+ // Durable entries, including retry metadata.
301
+ const outbox = await offline.peekOutbox()
302
+ for (const entry of outbox) {
303
+ console.log(entry.id, entry.retryCount, entry.lastError)
304
+ }
305
+ ```
306
+
307
+ These values describe local executor work, not backend confirmation. A normal
308
+ retriable error leaves the transaction queued. A `NonRetriableError` marks a
309
+ permanent failure, removes the outbox entry, and rolls back its optimistic
310
+ state.
311
+
203
312
  ## Migration from TanStack DB
204
313
 
205
314
  This package uses explicit offline transactions to provide offline capabilities:
@@ -218,9 +327,14 @@ const offline = startOfflineExecutor({
218
327
  },
219
328
  })
220
329
 
221
- const tx = offline.createOfflineTransaction({ mutationFnName: 'syncTodos' })
330
+ await offline.waitForInit()
331
+
332
+ const tx = offline.createOfflineTransaction({
333
+ mutationFnName: 'syncTodos',
334
+ autoCommit: false,
335
+ })
222
336
  tx.mutate(() => todoCollection.insert({ id: '1', text: 'Buy milk' }))
223
- await tx.commit() // Works offline!
337
+ await tx.commit() // Remains pending until the mutation function succeeds.
224
338
  ```
225
339
 
226
340
  ## Platform Support
@@ -234,10 +348,10 @@ await tx.commit() // Works offline!
234
348
 
235
349
  ### React Native
236
350
 
237
- - **React Native**: 0.60+ (tested with latest versions)
238
- - **Expo**: SDK 40+ (tested with latest versions)
351
+ - **React Native**: 0.70+ (package peer dependency)
352
+ - **Expo**: Use the React Native entry point
239
353
  - **Required peer dependency**: `@react-native-community/netinfo` for network connectivity detection
240
- - **Storage**: Uses AsyncStorage or custom storage adapters
354
+ - **Storage**: Supply a custom `StorageAdapter`, such as the [example AsyncStorage adapter](../../examples/react-native/offline-transactions/src/db/AsyncStorageAdapter.ts)
241
355
 
242
356
  ## License
243
357
 
@@ -15,6 +15,7 @@ const tracer = require("./telemetry/tracer.cjs");
15
15
  let OfflineExecutor$1 = class OfflineExecutor {
16
16
  constructor(config) {
17
17
  this.isLeaderState = false;
18
+ this.disposed = false;
18
19
  this.unsubscribeOnline = null;
19
20
  this.unsubscribeLeadership = null;
20
21
  this.pendingTransactionPromises = /* @__PURE__ */ new Map();
@@ -36,6 +37,8 @@ let OfflineExecutor$1 = class OfflineExecutor {
36
37
  this.initResolve = resolve;
37
38
  this.initReject = reject;
38
39
  });
40
+ void this.initPromise.catch(() => {
41
+ });
39
42
  this.initialize();
40
43
  }
41
44
  /**
@@ -123,12 +126,16 @@ let OfflineExecutor$1 = class OfflineExecutor {
123
126
  if (this.leaderElection) {
124
127
  this.unsubscribeLeadership = this.leaderElection.onLeadershipChange(
125
128
  (isLeader) => {
129
+ if (this.disposed || isLeader === this.isLeaderState) return;
126
130
  this.isLeaderState = isLeader;
131
+ if (!isLeader) this.executor?.pause();
127
132
  if (this.config.onLeadershipChange) {
128
133
  this.config.onLeadershipChange(isLeader);
129
134
  }
130
135
  if (isLeader) {
131
- this.loadAndReplayTransactions();
136
+ this.loadAndReplayTransactions().catch((error) => {
137
+ console.warn(`Failed to load and replay transactions:`, error);
138
+ });
132
139
  }
133
140
  }
134
141
  );
@@ -168,6 +175,11 @@ let OfflineExecutor$1 = class OfflineExecutor {
168
175
  return tracer.withSpan(`executor.initialize`, {}, async (span) => {
169
176
  try {
170
177
  const { storage, diagnostic } = await this.createStorage();
178
+ if (this.disposed) {
179
+ this.initResolve();
180
+ return;
181
+ }
182
+ ;
171
183
  this.storage = storage;
172
184
  this.storageDiagnostic = diagnostic;
173
185
  this.mode = diagnostic.mode;
@@ -190,6 +202,11 @@ let OfflineExecutor$1 = class OfflineExecutor {
190
202
  );
191
203
  this.leaderElection = this.createLeaderElection();
192
204
  const isLeader = await this.leaderElection.requestLeadership();
205
+ if (this.disposed) {
206
+ this.leaderElection.releaseLeadership();
207
+ this.initResolve();
208
+ return;
209
+ }
193
210
  this.isLeaderState = isLeader;
194
211
  span.setAttribute(`isLeader`, isLeader);
195
212
  this.setupEventListeners();
@@ -214,17 +231,13 @@ let OfflineExecutor$1 = class OfflineExecutor {
214
231
  if (!this.executor) {
215
232
  return;
216
233
  }
217
- try {
218
- await this.executor.loadPendingTransactions();
219
- this.executor.executeAll().catch((error) => {
220
- console.warn(`Failed to execute transactions:`, error);
221
- });
222
- } catch (error) {
223
- console.warn(`Failed to load and replay transactions:`, error);
224
- }
234
+ await this.executor.loadPendingTransactions();
235
+ this.executor.executeAll().catch((error) => {
236
+ console.warn(`Failed to execute transactions:`, error);
237
+ });
225
238
  }
226
239
  get isOfflineEnabled() {
227
- return this.mode === `offline` && this.isLeaderState;
240
+ return !this.disposed && this.mode === `offline` && this.isLeaderState;
228
241
  }
229
242
  /**
230
243
  * Wait for the executor to fully initialize.
@@ -297,7 +310,9 @@ let OfflineExecutor$1 = class OfflineExecutor {
297
310
  }
298
311
  try {
299
312
  await this.outbox.add(transaction);
300
- await this.executor.execute(transaction);
313
+ this.executor.execute(transaction).catch((error) => {
314
+ console.warn(`Failed to execute transactions:`, error);
315
+ });
301
316
  span.setAttribute(`result`, `persisted`);
302
317
  } catch (error) {
303
318
  console.error(
@@ -412,6 +427,8 @@ let OfflineExecutor$1 = class OfflineExecutor {
412
427
  return this.onlineDetector.isOnline();
413
428
  }
414
429
  dispose() {
430
+ this.disposed = true;
431
+ this.executor?.pause();
415
432
  for (const collection of Object.values(this.config.collections)) {
416
433
  collection.deferDataRefresh = null;
417
434
  }
@@ -1 +1 @@
1
- {"version":3,"file":"OfflineExecutor.cjs","sources":["../../src/OfflineExecutor.ts"],"sourcesContent":["// Storage adapters\nimport {\n createOptimisticAction,\n createTransaction,\n safeRandomUUID,\n} from '@tanstack/db'\nimport { IndexedDBAdapter } from './storage/IndexedDBAdapter'\nimport { LocalStorageAdapter } from './storage/LocalStorageAdapter'\n\n// Core components\nimport { OutboxManager } from './outbox/OutboxManager'\nimport { KeyScheduler } from './executor/KeyScheduler'\nimport { TransactionExecutor } from './executor/TransactionExecutor'\n\n// Coordination\nimport { WebLocksLeader } from './coordination/WebLocksLeader'\nimport { BroadcastChannelLeader } from './coordination/BroadcastChannelLeader'\n\n// Connectivity\nimport { WebOnlineDetector } from './connectivity/OnlineDetector'\n\n// API\nimport { OfflineTransaction as OfflineTransactionAPI } from './api/OfflineTransaction'\nimport { createOfflineAction } from './api/OfflineAction'\n\n// TanStack DB primitives\n\n// Replay\nimport { withNestedSpan, withSpan } from './telemetry/tracer'\nimport type {\n CreateOfflineActionOptions,\n CreateOfflineTransactionOptions,\n LeaderElection,\n OfflineConfig,\n OfflineMode,\n OfflineTransaction,\n OnlineDetector,\n StorageAdapter,\n StorageDiagnostic,\n} from './types'\nimport type { Transaction } from '@tanstack/db'\n\nexport class OfflineExecutor {\n private config: OfflineConfig\n\n // @ts-expect-error - Set during async initialization in initialize()\n private storage: StorageAdapter | null\n private outbox: OutboxManager | null\n private scheduler: KeyScheduler\n private executor: TransactionExecutor | null\n private leaderElection: LeaderElection | null\n private onlineDetector: OnlineDetector\n private isLeaderState = false\n private unsubscribeOnline: (() => void) | null = null\n private unsubscribeLeadership: (() => void) | null = null\n\n // Public diagnostic properties\n public readonly mode: OfflineMode\n public readonly storageDiagnostic: StorageDiagnostic\n\n // Track initialization completion\n private initPromise: Promise<void>\n private initResolve!: () => void\n private initReject!: (error: Error) => void\n\n // Coordination mechanism for blocking transactions\n private pendingTransactionPromises: Map<\n string,\n {\n promise: Promise<any>\n resolve: (result: any) => void\n reject: (error: Error) => void\n }\n > = new Map()\n\n // Track restoration transactions for cleanup when offline transactions complete\n private restorationTransactions: Map<string, Transaction> = new Map()\n\n constructor(config: OfflineConfig) {\n this.config = config\n this.scheduler = new KeyScheduler()\n this.onlineDetector = config.onlineDetector ?? new WebOnlineDetector()\n\n // Initialize as pending - will be set by async initialization\n this.storage = null\n this.outbox = null\n this.executor = null\n this.leaderElection = null\n\n // Temporary diagnostic - will be updated by async initialization\n this.mode = `offline`\n this.storageDiagnostic = {\n code: `STORAGE_AVAILABLE`,\n mode: `offline`,\n message: `Initializing storage...`,\n }\n\n // Create initialization promise\n this.initPromise = new Promise((resolve, reject) => {\n this.initResolve = resolve\n this.initReject = reject\n })\n\n this.initialize()\n }\n\n /**\n * Probe storage availability and create appropriate adapter.\n * Returns null if no storage is available (online-only mode).\n */\n private async createStorage(): Promise<{\n storage: StorageAdapter | null\n diagnostic: StorageDiagnostic\n }> {\n // If user provided custom storage, use it without probing\n if (this.config.storage) {\n return {\n storage: this.config.storage,\n diagnostic: {\n code: `STORAGE_AVAILABLE`,\n mode: `offline`,\n message: `Using custom storage adapter`,\n },\n }\n }\n\n // Probe IndexedDB first\n const idbProbe = await IndexedDBAdapter.probe()\n if (idbProbe.available) {\n return {\n storage: new IndexedDBAdapter(),\n diagnostic: {\n code: `STORAGE_AVAILABLE`,\n mode: `offline`,\n message: `Using IndexedDB for offline storage`,\n },\n }\n }\n\n // IndexedDB failed, try localStorage\n const lsProbe = LocalStorageAdapter.probe()\n if (lsProbe.available) {\n return {\n storage: new LocalStorageAdapter(),\n diagnostic: {\n code: `INDEXEDDB_UNAVAILABLE`,\n mode: `offline`,\n message: `IndexedDB unavailable, using localStorage fallback`,\n error: idbProbe.error,\n },\n }\n }\n\n // Both failed - determine the diagnostic code\n const isSecurityError =\n idbProbe.error?.name === `SecurityError` ||\n lsProbe.error?.name === `SecurityError`\n const isQuotaError =\n idbProbe.error?.name === `QuotaExceededError` ||\n lsProbe.error?.name === `QuotaExceededError`\n\n let code: StorageDiagnostic[`code`]\n let message: string\n\n if (isSecurityError) {\n code = `STORAGE_BLOCKED`\n message = `Storage blocked (private mode or security restrictions). Running in online-only mode.`\n } else if (isQuotaError) {\n code = `QUOTA_EXCEEDED`\n message = `Storage quota exceeded. Running in online-only mode.`\n } else {\n code = `UNKNOWN_ERROR`\n message = `Storage unavailable due to unknown error. Running in online-only mode.`\n }\n\n return {\n storage: null,\n diagnostic: {\n code,\n mode: `online-only`,\n message,\n error: idbProbe.error || lsProbe.error,\n },\n }\n }\n\n private createLeaderElection(): LeaderElection {\n if (this.config.leaderElection) {\n return this.config.leaderElection\n }\n\n if (WebLocksLeader.isSupported()) {\n return new WebLocksLeader()\n } else if (BroadcastChannelLeader.isSupported()) {\n return new BroadcastChannelLeader()\n } else {\n // Fallback: always be leader in environments without multi-tab support\n return {\n requestLeadership: () => Promise.resolve(true),\n releaseLeadership: () => {},\n isLeader: () => true,\n onLeadershipChange: () => () => {},\n }\n }\n }\n\n private setupEventListeners(): void {\n // Only set up leader election listeners if we have storage\n if (this.leaderElection) {\n this.unsubscribeLeadership = this.leaderElection.onLeadershipChange(\n (isLeader) => {\n this.isLeaderState = isLeader\n\n if (this.config.onLeadershipChange) {\n this.config.onLeadershipChange(isLeader)\n }\n\n if (isLeader) {\n this.loadAndReplayTransactions()\n }\n },\n )\n }\n\n this.unsubscribeOnline = this.onlineDetector.subscribe(() => {\n if (this.isOfflineEnabled && this.executor) {\n this.executor.resetRetryDelays()\n\n if (this.scheduler.getPendingCount() > 0) {\n const barrierPromise = this.executor.executeAll()\n\n for (const collection of Object.values(this.config.collections)) {\n collection.deferDataRefresh = barrierPromise\n }\n\n barrierPromise\n .catch((error) => {\n console.warn(\n `Failed to execute transactions on connectivity change:`,\n error,\n )\n })\n .finally(() => {\n for (const collection of Object.values(this.config.collections)) {\n if (collection.deferDataRefresh === barrierPromise) {\n collection.deferDataRefresh = null\n }\n }\n })\n } else {\n this.executor.executeAll().catch((error) => {\n console.warn(\n `Failed to execute transactions on connectivity change:`,\n error,\n )\n })\n }\n }\n })\n }\n\n private async initialize(): Promise<void> {\n return withSpan(`executor.initialize`, {}, async (span) => {\n try {\n // Probe storage and create adapter\n const { storage, diagnostic } = await this.createStorage()\n\n // Cast to writable to set readonly properties\n ;(this as any).storage = storage\n ;(this as any).storageDiagnostic = diagnostic\n ;(this as any).mode = diagnostic.mode\n\n span.setAttribute(`storage.mode`, diagnostic.mode)\n span.setAttribute(`storage.code`, diagnostic.code)\n\n if (!storage) {\n // Online-only mode - notify callback and skip offline setup\n if (this.config.onStorageFailure) {\n this.config.onStorageFailure(diagnostic)\n }\n span.setAttribute(`result`, `online-only`)\n this.initResolve()\n return\n }\n\n // Storage available - set up offline components\n this.outbox = new OutboxManager(storage, this.config.collections)\n this.executor = new TransactionExecutor(\n this.scheduler,\n this.outbox,\n this.config,\n this,\n )\n this.leaderElection = this.createLeaderElection()\n\n // Request leadership first\n const isLeader = await this.leaderElection.requestLeadership()\n this.isLeaderState = isLeader\n span.setAttribute(`isLeader`, isLeader)\n\n // Set up event listeners after leadership is established\n // This prevents the callback from being called multiple times\n this.setupEventListeners()\n\n // Notify initial leadership state\n if (this.config.onLeadershipChange) {\n this.config.onLeadershipChange(isLeader)\n }\n\n if (isLeader) {\n await this.loadAndReplayTransactions()\n }\n span.setAttribute(`result`, `offline-enabled`)\n this.initResolve()\n } catch (error) {\n console.warn(`Failed to initialize offline executor:`, error)\n span.setAttribute(`result`, `failed`)\n this.initReject(\n error instanceof Error ? error : new Error(String(error)),\n )\n }\n })\n }\n\n private async loadAndReplayTransactions(): Promise<void> {\n if (!this.executor) {\n return\n }\n\n try {\n // Load pending transactions and restore optimistic state\n await this.executor.loadPendingTransactions()\n\n // Start execution in the background - don't await to avoid blocking initialization\n // The transactions will execute and complete asynchronously\n this.executor.executeAll().catch((error) => {\n console.warn(`Failed to execute transactions:`, error)\n })\n } catch (error) {\n console.warn(`Failed to load and replay transactions:`, error)\n }\n }\n\n get isOfflineEnabled(): boolean {\n return this.mode === `offline` && this.isLeaderState\n }\n\n /**\n * Wait for the executor to fully initialize.\n * This ensures that pending transactions are loaded and optimistic state is restored.\n */\n async waitForInit(): Promise<void> {\n return this.initPromise\n }\n\n createOfflineTransaction(\n options: CreateOfflineTransactionOptions,\n ): Transaction | OfflineTransactionAPI {\n const mutationFn = this.config.mutationFns[options.mutationFnName]\n\n if (!mutationFn) {\n throw new Error(`Unknown mutation function: ${options.mutationFnName}`)\n }\n\n // Check leadership immediately and use the appropriate primitive\n if (!this.isOfflineEnabled) {\n // Non-leader: use createTransaction directly with the resolved mutation function\n // We need to wrap it to add the idempotency key\n return createTransaction({\n autoCommit: options.autoCommit ?? true,\n mutationFn: (params) =>\n mutationFn({\n ...params,\n idempotencyKey: options.idempotencyKey || safeRandomUUID(),\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: safeRandomUUID(),\n }),\n onMutate: options.onMutate,\n })\n return action(variables)\n }\n\n // Leader: use the offline action wrapper\n const action = createOfflineAction(\n options,\n mutationFn,\n this.persistTransaction.bind(this),\n this,\n )\n return action(variables)\n }\n }\n\n private async persistTransaction(\n transaction: OfflineTransaction,\n ): Promise<void> {\n // Wait for initialization to complete\n await this.initPromise\n\n return withNestedSpan(\n `executor.persistTransaction`,\n {\n 'transaction.id': transaction.id,\n 'transaction.mutationFnName': transaction.mutationFnName,\n },\n async (span) => {\n if (!this.isOfflineEnabled || !this.outbox || !this.executor) {\n span.setAttribute(`result`, `skipped_not_leader`)\n this.resolveTransaction(transaction.id, undefined)\n return\n }\n\n try {\n await this.outbox.add(transaction)\n await this.executor.execute(transaction)\n span.setAttribute(`result`, `persisted`)\n } catch (error) {\n console.error(\n `Failed to persist offline transaction ${transaction.id}:`,\n error,\n )\n span.setAttribute(`result`, `failed`)\n throw error\n }\n },\n )\n }\n\n // Method for OfflineTransaction to wait for completion\n async waitForTransactionCompletion(transactionId: string): Promise<any> {\n const existing = this.pendingTransactionPromises.get(transactionId)\n if (existing) {\n return existing.promise\n }\n\n const deferred: {\n promise: Promise<any>\n resolve: (result: any) => void\n reject: (error: Error) => void\n } = {} as any\n\n deferred.promise = new Promise((resolve, reject) => {\n deferred.resolve = resolve\n deferred.reject = reject\n })\n\n this.pendingTransactionPromises.set(transactionId, deferred)\n return deferred.promise\n }\n\n // Method for TransactionExecutor to signal completion\n resolveTransaction(transactionId: string, result: any): void {\n const deferred = this.pendingTransactionPromises.get(transactionId)\n if (deferred) {\n deferred.resolve(result)\n this.pendingTransactionPromises.delete(transactionId)\n }\n\n // Clean up the restoration transaction - the sync will provide authoritative data\n this.cleanupRestorationTransaction(transactionId)\n }\n\n // Method for TransactionExecutor to signal failure\n rejectTransaction(transactionId: string, error: Error): void {\n const deferred = this.pendingTransactionPromises.get(transactionId)\n if (deferred) {\n deferred.reject(error)\n this.pendingTransactionPromises.delete(transactionId)\n }\n\n // Clean up the restoration transaction and rollback optimistic state\n this.cleanupRestorationTransaction(transactionId, true)\n }\n\n // Method for TransactionExecutor to register restoration transactions\n registerRestorationTransaction(\n offlineTransactionId: string,\n restorationTransaction: Transaction,\n ): void {\n this.restorationTransactions.set(\n offlineTransactionId,\n restorationTransaction,\n )\n }\n\n private cleanupRestorationTransaction(\n transactionId: string,\n shouldRollback = false,\n ): void {\n const restorationTx = this.restorationTransactions.get(transactionId)\n if (!restorationTx) {\n return\n }\n\n this.restorationTransactions.delete(transactionId)\n\n if (shouldRollback) {\n restorationTx.rollback()\n return\n }\n\n // Mark as completed so recomputeOptimisticState removes it from consideration.\n // The actual data will come from the sync.\n restorationTx.setState(`completed`)\n\n // Remove from each collection's transaction map and recompute\n const touchedCollections = new Set<string>()\n for (const mutation of restorationTx.mutations) {\n // Defensive check for corrupted deserialized data\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!mutation.collection) {\n continue\n }\n const collectionId = mutation.collection.id\n if (touchedCollections.has(collectionId)) {\n continue\n }\n touchedCollections.add(collectionId)\n mutation.collection._state.transactions.delete(restorationTx.id)\n mutation.collection._state.recomputeOptimisticState(false)\n }\n }\n\n async removeFromOutbox(id: string): Promise<void> {\n if (!this.outbox) {\n return\n }\n await this.outbox.remove(id)\n }\n\n async peekOutbox(): Promise<Array<OfflineTransaction>> {\n if (!this.outbox) {\n return []\n }\n return this.outbox.getAll()\n }\n\n async clearOutbox(): Promise<void> {\n if (!this.outbox || !this.executor) {\n return\n }\n await this.outbox.clear()\n this.executor.clear()\n }\n\n getPendingCount(): number {\n if (!this.executor) {\n return 0\n }\n return this.executor.getPendingCount()\n }\n\n getRunningCount(): number {\n if (!this.executor) {\n return 0\n }\n return this.executor.getRunningCount()\n }\n\n getOnlineDetector(): OnlineDetector {\n return this.onlineDetector\n }\n\n isOnline(): boolean {\n return this.onlineDetector.isOnline()\n }\n\n dispose(): void {\n for (const collection of Object.values(this.config.collections)) {\n collection.deferDataRefresh = null\n }\n\n if (this.unsubscribeOnline) {\n this.unsubscribeOnline()\n this.unsubscribeOnline = null\n }\n\n if (this.unsubscribeLeadership) {\n this.unsubscribeLeadership()\n this.unsubscribeLeadership = null\n }\n\n if (this.leaderElection) {\n this.leaderElection.releaseLeadership()\n\n if (`dispose` in this.leaderElection) {\n ;(this.leaderElection as any).dispose()\n }\n }\n\n this.onlineDetector.dispose()\n }\n}\n\nexport function startOfflineExecutor(config: OfflineConfig): OfflineExecutor {\n return new OfflineExecutor(config)\n}\n"],"names":["KeyScheduler","WebOnlineDetector","IndexedDBAdapter","LocalStorageAdapter","WebLocksLeader","BroadcastChannelLeader","withSpan","OutboxManager","TransactionExecutor","createTransaction","safeRandomUUID","OfflineTransactionAPI","action","createOptimisticAction","createOfflineAction","withNestedSpan","OfflineExecutor"],"mappings":"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"}
1
+ {"version":3,"file":"OfflineExecutor.cjs","sources":["../../src/OfflineExecutor.ts"],"sourcesContent":["// Storage adapters\nimport {\n createOptimisticAction,\n createTransaction,\n safeRandomUUID,\n} from '@tanstack/db'\nimport { IndexedDBAdapter } from './storage/IndexedDBAdapter'\nimport { LocalStorageAdapter } from './storage/LocalStorageAdapter'\n\n// Core components\nimport { OutboxManager } from './outbox/OutboxManager'\nimport { KeyScheduler } from './executor/KeyScheduler'\nimport { TransactionExecutor } from './executor/TransactionExecutor'\n\n// Coordination\nimport { WebLocksLeader } from './coordination/WebLocksLeader'\nimport { BroadcastChannelLeader } from './coordination/BroadcastChannelLeader'\n\n// Connectivity\nimport { WebOnlineDetector } from './connectivity/OnlineDetector'\n\n// API\nimport { OfflineTransaction as OfflineTransactionAPI } from './api/OfflineTransaction'\nimport { createOfflineAction } from './api/OfflineAction'\n\n// TanStack DB primitives\n\n// Replay\nimport { withNestedSpan, withSpan } from './telemetry/tracer'\nimport type {\n CreateOfflineActionOptions,\n CreateOfflineTransactionOptions,\n LeaderElection,\n OfflineConfig,\n OfflineMode,\n OfflineTransaction,\n OnlineDetector,\n StorageAdapter,\n StorageDiagnostic,\n} from './types'\nimport type { Transaction } from '@tanstack/db'\n\nexport class OfflineExecutor {\n private config: OfflineConfig\n\n // @ts-expect-error - Set during async initialization in initialize()\n private storage: StorageAdapter | null\n private outbox: OutboxManager | null\n private scheduler: KeyScheduler\n private executor: TransactionExecutor | null\n private leaderElection: LeaderElection | null\n private onlineDetector: OnlineDetector\n private isLeaderState = false\n private disposed = false\n private unsubscribeOnline: (() => void) | null = null\n private unsubscribeLeadership: (() => void) | null = null\n\n // Public diagnostic properties\n public readonly mode: OfflineMode\n public readonly storageDiagnostic: StorageDiagnostic\n\n // Track initialization completion\n private initPromise: Promise<void>\n private initResolve!: () => void\n private initReject!: (error: Error) => void\n\n // Coordination mechanism for blocking transactions\n private pendingTransactionPromises: Map<\n string,\n {\n promise: Promise<any>\n resolve: (result: any) => void\n reject: (error: Error) => void\n }\n > = new Map()\n\n // Track restoration transactions for cleanup when offline transactions complete\n private restorationTransactions: Map<string, Transaction> = new Map()\n\n constructor(config: OfflineConfig) {\n this.config = config\n this.scheduler = new KeyScheduler()\n this.onlineDetector = config.onlineDetector ?? new WebOnlineDetector()\n\n // Initialize as pending - will be set by async initialization\n this.storage = null\n this.outbox = null\n this.executor = null\n this.leaderElection = null\n\n // Temporary diagnostic - will be updated by async initialization\n this.mode = `offline`\n this.storageDiagnostic = {\n code: `STORAGE_AVAILABLE`,\n mode: `offline`,\n message: `Initializing storage...`,\n }\n\n // Create initialization promise\n this.initPromise = new Promise((resolve, reject) => {\n this.initResolve = resolve\n this.initReject = reject\n })\n // Handle constructor-started rejection; waitForInit still observes it.\n void this.initPromise.catch(() => {})\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 // A custom elector may repeat the initial result while replay is active.\n if (this.disposed || isLeader === this.isLeaderState) return\n this.isLeaderState = isLeader\n if (!isLeader) this.executor?.pause()\n\n if (this.config.onLeadershipChange) {\n this.config.onLeadershipChange(isLeader)\n }\n\n if (isLeader) {\n this.loadAndReplayTransactions().catch((error) => {\n console.warn(`Failed to load and replay transactions:`, error)\n })\n }\n },\n )\n }\n\n this.unsubscribeOnline = this.onlineDetector.subscribe(() => {\n if (this.isOfflineEnabled && this.executor) {\n this.executor.resetRetryDelays()\n\n if (this.scheduler.getPendingCount() > 0) {\n const barrierPromise = this.executor.executeAll()\n\n for (const collection of Object.values(this.config.collections)) {\n collection.deferDataRefresh = barrierPromise\n }\n\n barrierPromise\n .catch((error) => {\n console.warn(\n `Failed to execute transactions on connectivity change:`,\n error,\n )\n })\n .finally(() => {\n for (const collection of Object.values(this.config.collections)) {\n if (collection.deferDataRefresh === barrierPromise) {\n collection.deferDataRefresh = null\n }\n }\n })\n } else {\n this.executor.executeAll().catch((error) => {\n console.warn(\n `Failed to execute transactions on connectivity change:`,\n error,\n )\n })\n }\n }\n })\n }\n\n private async initialize(): Promise<void> {\n return withSpan(`executor.initialize`, {}, async (span) => {\n try {\n // Probe storage and create adapter\n const { storage, diagnostic } = await this.createStorage()\n if (this.disposed) {\n this.initResolve()\n return\n }\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 // Disposal may have run while leadership was pending.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (this.disposed) {\n this.leaderElection.releaseLeadership()\n this.initResolve()\n return\n }\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 // Startup must observe a failed read instead of reporting an empty outbox.\n await this.executor.loadPendingTransactions()\n\n // Replay completion is separate from initialization and durable admission.\n this.executor.executeAll().catch((error) => {\n console.warn(`Failed to execute transactions:`, error)\n })\n }\n\n get isOfflineEnabled(): boolean {\n return !this.disposed && this.mode === `offline` && this.isLeaderState\n }\n\n /**\n * Wait for the executor to fully initialize.\n * This ensures that pending transactions are loaded and optimistic state is restored.\n */\n async waitForInit(): Promise<void> {\n return this.initPromise\n }\n\n createOfflineTransaction(\n options: CreateOfflineTransactionOptions,\n ): Transaction | OfflineTransactionAPI {\n const mutationFn = this.config.mutationFns[options.mutationFnName]\n\n if (!mutationFn) {\n throw new Error(`Unknown mutation function: ${options.mutationFnName}`)\n }\n\n // Check leadership immediately and use the appropriate primitive\n if (!this.isOfflineEnabled) {\n // Non-leader: use createTransaction directly with the resolved mutation function\n // We need to wrap it to add the idempotency key\n return createTransaction({\n autoCommit: options.autoCommit ?? true,\n mutationFn: (params) =>\n mutationFn({\n ...params,\n idempotencyKey: options.idempotencyKey || safeRandomUUID(),\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: safeRandomUUID(),\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 // A shared queue error belongs to its execution, not every caller\n // that durably admitted a transaction. Per-ID signals settle callers.\n this.executor.execute(transaction).catch((error) => {\n console.warn(`Failed to execute transactions:`, error)\n })\n span.setAttribute(`result`, `persisted`)\n } catch (error) {\n console.error(\n `Failed to persist offline transaction ${transaction.id}:`,\n error,\n )\n span.setAttribute(`result`, `failed`)\n throw error\n }\n },\n )\n }\n\n // Method for OfflineTransaction to wait for completion\n async waitForTransactionCompletion(transactionId: string): Promise<any> {\n const existing = this.pendingTransactionPromises.get(transactionId)\n if (existing) {\n return existing.promise\n }\n\n const deferred: {\n promise: Promise<any>\n resolve: (result: any) => void\n reject: (error: Error) => void\n } = {} as any\n\n deferred.promise = new Promise((resolve, reject) => {\n deferred.resolve = resolve\n deferred.reject = reject\n })\n\n this.pendingTransactionPromises.set(transactionId, deferred)\n return deferred.promise\n }\n\n // Method for TransactionExecutor to signal completion\n resolveTransaction(transactionId: string, result: any): void {\n const deferred = this.pendingTransactionPromises.get(transactionId)\n if (deferred) {\n deferred.resolve(result)\n this.pendingTransactionPromises.delete(transactionId)\n }\n\n // Clean up the restoration transaction - the sync will provide authoritative data\n this.cleanupRestorationTransaction(transactionId)\n }\n\n // Method for TransactionExecutor to signal failure\n rejectTransaction(transactionId: string, error: Error): void {\n const deferred = this.pendingTransactionPromises.get(transactionId)\n if (deferred) {\n deferred.reject(error)\n this.pendingTransactionPromises.delete(transactionId)\n }\n\n // Clean up the restoration transaction and rollback optimistic state\n this.cleanupRestorationTransaction(transactionId, true)\n }\n\n // Method for TransactionExecutor to register restoration transactions\n registerRestorationTransaction(\n offlineTransactionId: string,\n restorationTransaction: Transaction,\n ): void {\n this.restorationTransactions.set(\n offlineTransactionId,\n restorationTransaction,\n )\n }\n\n private cleanupRestorationTransaction(\n transactionId: string,\n shouldRollback = false,\n ): void {\n const restorationTx = this.restorationTransactions.get(transactionId)\n if (!restorationTx) {\n return\n }\n\n this.restorationTransactions.delete(transactionId)\n\n if (shouldRollback) {\n restorationTx.rollback()\n return\n }\n\n // Mark as completed so recomputeOptimisticState removes it from consideration.\n // The actual data will come from the sync.\n restorationTx.setState(`completed`)\n\n // Remove from each collection's transaction map and recompute\n const touchedCollections = new Set<string>()\n for (const mutation of restorationTx.mutations) {\n // Defensive check for corrupted deserialized data\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!mutation.collection) {\n continue\n }\n const collectionId = mutation.collection.id\n if (touchedCollections.has(collectionId)) {\n continue\n }\n touchedCollections.add(collectionId)\n mutation.collection._state.transactions.delete(restorationTx.id)\n mutation.collection._state.recomputeOptimisticState(false)\n }\n }\n\n async removeFromOutbox(id: string): Promise<void> {\n if (!this.outbox) {\n return\n }\n await this.outbox.remove(id)\n }\n\n async peekOutbox(): Promise<Array<OfflineTransaction>> {\n if (!this.outbox) {\n return []\n }\n return this.outbox.getAll()\n }\n\n async clearOutbox(): Promise<void> {\n if (!this.outbox || !this.executor) {\n return\n }\n await this.outbox.clear()\n this.executor.clear()\n }\n\n getPendingCount(): number {\n if (!this.executor) {\n return 0\n }\n return this.executor.getPendingCount()\n }\n\n getRunningCount(): number {\n if (!this.executor) {\n return 0\n }\n return this.executor.getRunningCount()\n }\n\n getOnlineDetector(): OnlineDetector {\n return this.onlineDetector\n }\n\n isOnline(): boolean {\n return this.onlineDetector.isOnline()\n }\n\n dispose(): void {\n this.disposed = true\n this.executor?.pause()\n for (const collection of Object.values(this.config.collections)) {\n collection.deferDataRefresh = null\n }\n\n if (this.unsubscribeOnline) {\n this.unsubscribeOnline()\n this.unsubscribeOnline = null\n }\n\n if (this.unsubscribeLeadership) {\n this.unsubscribeLeadership()\n this.unsubscribeLeadership = null\n }\n\n if (this.leaderElection) {\n this.leaderElection.releaseLeadership()\n\n if (`dispose` in this.leaderElection) {\n ;(this.leaderElection as any).dispose()\n }\n }\n\n this.onlineDetector.dispose()\n }\n}\n\nexport function startOfflineExecutor(config: OfflineConfig): OfflineExecutor {\n return new OfflineExecutor(config)\n}\n"],"names":["KeyScheduler","WebOnlineDetector","IndexedDBAdapter","LocalStorageAdapter","WebLocksLeader","BroadcastChannelLeader","withSpan","OutboxManager","TransactionExecutor","createTransaction","safeRandomUUID","OfflineTransactionAPI","action","createOptimisticAction","createOfflineAction","withNestedSpan","OfflineExecutor"],"mappings":";;;;;;;;;;;;;;AA0CO,IAAA,oBAAA,MAAM,gBAAgB;AAAA,EAqC3B,YAAY,QAAuB;AA3BnC,SAAQ,gBAAgB;AACxB,SAAQ,WAAW;AACnB,SAAQ,oBAAyC;AACjD,SAAQ,wBAA6C;AAYrD,SAAQ,iDAOA,IAAA;AAGR,SAAQ,8CAAwD,IAAA;AAG9D,SAAK,SAAS;AACd,SAAK,YAAY,IAAIA,0BAAA;AACrB,SAAK,iBAAiB,OAAO,kBAAkB,IAAIC,eAAAA,kBAAA;AAGnD,SAAK,UAAU;AACf,SAAK,SAAS;AACd,SAAK,WAAW;AAChB,SAAK,iBAAiB;AAGtB,SAAK,OAAO;AACZ,SAAK,oBAAoB;AAAA,MACvB,MAAM;AAAA,MACN,MAAM;AAAA,MACN,SAAS;AAAA,IAAA;AAIX,SAAK,cAAc,IAAI,QAAQ,CAAC,SAAS,WAAW;AAClD,WAAK,cAAc;AACnB,WAAK,aAAa;AAAA,IACpB,CAAC;AAED,SAAK,KAAK,YAAY,MAAM,MAAM;AAAA,IAAC,CAAC;AACpC,SAAK,WAAA;AAAA,EACP;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,gBAGX;AAED,QAAI,KAAK,OAAO,SAAS;AACvB,aAAO;AAAA,QACL,SAAS,KAAK,OAAO;AAAA,QACrB,YAAY;AAAA,UACV,MAAM;AAAA,UACN,MAAM;AAAA,UACN,SAAS;AAAA,QAAA;AAAA,MACX;AAAA,IAEJ;AAGA,UAAM,WAAW,MAAMC,iBAAAA,iBAAiB,MAAA;AACxC,QAAI,SAAS,WAAW;AACtB,aAAO;AAAA,QACL,SAAS,IAAIA,iBAAAA,iBAAA;AAAA,QACb,YAAY;AAAA,UACV,MAAM;AAAA,UACN,MAAM;AAAA,UACN,SAAS;AAAA,QAAA;AAAA,MACX;AAAA,IAEJ;AAGA,UAAM,UAAUC,oBAAAA,oBAAoB,MAAA;AACpC,QAAI,QAAQ,WAAW;AACrB,aAAO;AAAA,QACL,SAAS,IAAIA,oBAAAA,oBAAA;AAAA,QACb,YAAY;AAAA,UACV,MAAM;AAAA,UACN,MAAM;AAAA,UACN,SAAS;AAAA,UACT,OAAO,SAAS;AAAA,QAAA;AAAA,MAClB;AAAA,IAEJ;AAGA,UAAM,kBACJ,SAAS,OAAO,SAAS,mBACzB,QAAQ,OAAO,SAAS;AAC1B,UAAM,eACJ,SAAS,OAAO,SAAS,wBACzB,QAAQ,OAAO,SAAS;AAE1B,QAAI;AACJ,QAAI;AAEJ,QAAI,iBAAiB;AACnB,aAAO;AACP,gBAAU;AAAA,IACZ,WAAW,cAAc;AACvB,aAAO;AACP,gBAAU;AAAA,IACZ,OAAO;AACL,aAAO;AACP,gBAAU;AAAA,IACZ;AAEA,WAAO;AAAA,MACL,SAAS;AAAA,MACT,YAAY;AAAA,QACV;AAAA,QACA,MAAM;AAAA,QACN;AAAA,QACA,OAAO,SAAS,SAAS,QAAQ;AAAA,MAAA;AAAA,IACnC;AAAA,EAEJ;AAAA,EAEQ,uBAAuC;AAC7C,QAAI,KAAK,OAAO,gBAAgB;AAC9B,aAAO,KAAK,OAAO;AAAA,IACrB;AAEA,QAAIC,eAAAA,eAAe,eAAe;AAChC,aAAO,IAAIA,eAAAA,eAAA;AAAA,IACb,WAAWC,8CAAuB,eAAe;AAC/C,aAAO,IAAIA,uBAAAA,uBAAA;AAAA,IACb,OAAO;AAEL,aAAO;AAAA,QACL,mBAAmB,MAAM,QAAQ,QAAQ,IAAI;AAAA,QAC7C,mBAAmB,MAAM;AAAA,QAAC;AAAA,QAC1B,UAAU,MAAM;AAAA,QAChB,oBAAoB,MAAM,MAAM;AAAA,QAAC;AAAA,MAAA;AAAA,IAErC;AAAA,EACF;AAAA,EAEQ,sBAA4B;AAElC,QAAI,KAAK,gBAAgB;AACvB,WAAK,wBAAwB,KAAK,eAAe;AAAA,QAC/C,CAAC,aAAa;AAEZ,cAAI,KAAK,YAAY,aAAa,KAAK,cAAe;AACtD,eAAK,gBAAgB;AACrB,cAAI,CAAC,SAAU,MAAK,UAAU,MAAA;AAE9B,cAAI,KAAK,OAAO,oBAAoB;AAClC,iBAAK,OAAO,mBAAmB,QAAQ;AAAA,UACzC;AAEA,cAAI,UAAU;AACZ,iBAAK,0BAAA,EAA4B,MAAM,CAAC,UAAU;AAChD,sBAAQ,KAAK,2CAA2C,KAAK;AAAA,YAC/D,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MAAA;AAAA,IAEJ;AAEA,SAAK,oBAAoB,KAAK,eAAe,UAAU,MAAM;AAC3D,UAAI,KAAK,oBAAoB,KAAK,UAAU;AAC1C,aAAK,SAAS,iBAAA;AAEd,YAAI,KAAK,UAAU,gBAAA,IAAoB,GAAG;AACxC,gBAAM,iBAAiB,KAAK,SAAS,WAAA;AAErC,qBAAW,cAAc,OAAO,OAAO,KAAK,OAAO,WAAW,GAAG;AAC/D,uBAAW,mBAAmB;AAAA,UAChC;AAEA,yBACG,MAAM,CAAC,UAAU;AAChB,oBAAQ;AAAA,cACN;AAAA,cACA;AAAA,YAAA;AAAA,UAEJ,CAAC,EACA,QAAQ,MAAM;AACb,uBAAW,cAAc,OAAO,OAAO,KAAK,OAAO,WAAW,GAAG;AAC/D,kBAAI,WAAW,qBAAqB,gBAAgB;AAClD,2BAAW,mBAAmB;AAAA,cAChC;AAAA,YACF;AAAA,UACF,CAAC;AAAA,QACL,OAAO;AACL,eAAK,SAAS,WAAA,EAAa,MAAM,CAAC,UAAU;AAC1C,oBAAQ;AAAA,cACN;AAAA,cACA;AAAA,YAAA;AAAA,UAEJ,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,aAA4B;AACxC,WAAOC,OAAAA,SAAS,uBAAuB,CAAA,GAAI,OAAO,SAAS;AACzD,UAAI;AAEF,cAAM,EAAE,SAAS,WAAA,IAAe,MAAM,KAAK,cAAA;AAC3C,YAAI,KAAK,UAAU;AACjB,eAAK,YAAA;AACL;AAAA,QACF;AAGA;AAAE,aAAa,UAAU;AACvB,aAAa,oBAAoB;AACjC,aAAa,OAAO,WAAW;AAEjC,aAAK,aAAa,gBAAgB,WAAW,IAAI;AACjD,aAAK,aAAa,gBAAgB,WAAW,IAAI;AAEjD,YAAI,CAAC,SAAS;AAEZ,cAAI,KAAK,OAAO,kBAAkB;AAChC,iBAAK,OAAO,iBAAiB,UAAU;AAAA,UACzC;AACA,eAAK,aAAa,UAAU,aAAa;AACzC,eAAK,YAAA;AACL;AAAA,QACF;AAGA,aAAK,SAAS,IAAIC,cAAAA,cAAc,SAAS,KAAK,OAAO,WAAW;AAChE,aAAK,WAAW,IAAIC,oBAAAA;AAAAA,UAClB,KAAK;AAAA,UACL,KAAK;AAAA,UACL,KAAK;AAAA,UACL;AAAA,QAAA;AAEF,aAAK,iBAAiB,KAAK,qBAAA;AAG3B,cAAM,WAAW,MAAM,KAAK,eAAe,kBAAA;AAG3C,YAAI,KAAK,UAAU;AACjB,eAAK,eAAe,kBAAA;AACpB,eAAK,YAAA;AACL;AAAA,QACF;AACA,aAAK,gBAAgB;AACrB,aAAK,aAAa,YAAY,QAAQ;AAItC,aAAK,oBAAA;AAGL,YAAI,KAAK,OAAO,oBAAoB;AAClC,eAAK,OAAO,mBAAmB,QAAQ;AAAA,QACzC;AAEA,YAAI,UAAU;AACZ,gBAAM,KAAK,0BAAA;AAAA,QACb;AACA,aAAK,aAAa,UAAU,iBAAiB;AAC7C,aAAK,YAAA;AAAA,MACP,SAAS,OAAO;AACd,gBAAQ,KAAK,0CAA0C,KAAK;AAC5D,aAAK,aAAa,UAAU,QAAQ;AACpC,aAAK;AAAA,UACH,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAAA,QAAA;AAAA,MAE5D;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,4BAA2C;AACvD,QAAI,CAAC,KAAK,UAAU;AAClB;AAAA,IACF;AAGA,UAAM,KAAK,SAAS,wBAAA;AAGpB,SAAK,SAAS,WAAA,EAAa,MAAM,CAAC,UAAU;AAC1C,cAAQ,KAAK,mCAAmC,KAAK;AAAA,IACvD,CAAC;AAAA,EACH;AAAA,EAEA,IAAI,mBAA4B;AAC9B,WAAO,CAAC,KAAK,YAAY,KAAK,SAAS,aAAa,KAAK;AAAA,EAC3D;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,cAA6B;AACjC,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,yBACE,SACqC;AACrC,UAAM,aAAa,KAAK,OAAO,YAAY,QAAQ,cAAc;AAEjE,QAAI,CAAC,YAAY;AACf,YAAM,IAAI,MAAM,8BAA8B,QAAQ,cAAc,EAAE;AAAA,IACxE;AAGA,QAAI,CAAC,KAAK,kBAAkB;AAG1B,aAAOC,qBAAkB;AAAA,QACvB,YAAY,QAAQ,cAAc;AAAA,QAClC,YAAY,CAAC,WACX,WAAW;AAAA,UACT,GAAG;AAAA,UACH,gBAAgB,QAAQ,kBAAkBC,GAAAA,eAAA;AAAA,QAAe,CAC1D;AAAA,QACH,UAAU,QAAQ;AAAA,MAAA,CACnB;AAAA,IACH;AAGA,WAAO,IAAIC,mBAAAA;AAAAA,MACT;AAAA,MACA;AAAA,MACA,KAAK,mBAAmB,KAAK,IAAI;AAAA,MACjC;AAAA,IAAA;AAAA,EAEJ;AAAA,EAEA,oBAAuB,SAAwC;AAC7D,UAAM,aAAa,KAAK,OAAO,YAAY,QAAQ,cAAc;AAEjE,QAAI,CAAC,YAAY;AACf,YAAM,IAAI,MAAM,8BAA8B,QAAQ,cAAc,EAAE;AAAA,IACxE;AAGA,WAAO,CAAC,cAAiB;AAEvB,UAAI,CAAC,KAAK,kBAAkB;AAE1B,cAAMC,UAASC,GAAAA,uBAAuB;AAAA,UACpC,YAAY,CAAC,MAAM,WACjB,WAAW;AAAA,YACT,GAAG;AAAA,YACH,GAAG;AAAA,YACH,gBAAgBH,GAAAA,eAAA;AAAA,UAAe,CAChC;AAAA,UACH,UAAU,QAAQ;AAAA,QAAA,CACnB;AACD,eAAOE,QAAO,SAAS;AAAA,MACzB;AAGA,YAAM,SAASE,cAAAA;AAAAA,QACb;AAAA,QACA;AAAA,QACA,KAAK,mBAAmB,KAAK,IAAI;AAAA,QACjC;AAAA,MAAA;AAEF,aAAO,OAAO,SAAS;AAAA,IACzB;AAAA,EACF;AAAA,EAEA,MAAc,mBACZ,aACe;AAEf,UAAM,KAAK;AAEX,WAAOC,OAAAA;AAAAA,MACL;AAAA,MACA;AAAA,QACE,kBAAkB,YAAY;AAAA,QAC9B,8BAA8B,YAAY;AAAA,MAAA;AAAA,MAE5C,OAAO,SAAS;AACd,YAAI,CAAC,KAAK,oBAAoB,CAAC,KAAK,UAAU,CAAC,KAAK,UAAU;AAC5D,eAAK,aAAa,UAAU,oBAAoB;AAChD,eAAK,mBAAmB,YAAY,IAAI,MAAS;AACjD;AAAA,QACF;AAEA,YAAI;AACF,gBAAM,KAAK,OAAO,IAAI,WAAW;AAGjC,eAAK,SAAS,QAAQ,WAAW,EAAE,MAAM,CAAC,UAAU;AAClD,oBAAQ,KAAK,mCAAmC,KAAK;AAAA,UACvD,CAAC;AACD,eAAK,aAAa,UAAU,WAAW;AAAA,QACzC,SAAS,OAAO;AACd,kBAAQ;AAAA,YACN,yCAAyC,YAAY,EAAE;AAAA,YACvD;AAAA,UAAA;AAEF,eAAK,aAAa,UAAU,QAAQ;AACpC,gBAAM;AAAA,QACR;AAAA,MACF;AAAA,IAAA;AAAA,EAEJ;AAAA;AAAA,EAGA,MAAM,6BAA6B,eAAqC;AACtE,UAAM,WAAW,KAAK,2BAA2B,IAAI,aAAa;AAClE,QAAI,UAAU;AACZ,aAAO,SAAS;AAAA,IAClB;AAEA,UAAM,WAIF,CAAA;AAEJ,aAAS,UAAU,IAAI,QAAQ,CAAC,SAAS,WAAW;AAClD,eAAS,UAAU;AACnB,eAAS,SAAS;AAAA,IACpB,CAAC;AAED,SAAK,2BAA2B,IAAI,eAAe,QAAQ;AAC3D,WAAO,SAAS;AAAA,EAClB;AAAA;AAAA,EAGA,mBAAmB,eAAuB,QAAmB;AAC3D,UAAM,WAAW,KAAK,2BAA2B,IAAI,aAAa;AAClE,QAAI,UAAU;AACZ,eAAS,QAAQ,MAAM;AACvB,WAAK,2BAA2B,OAAO,aAAa;AAAA,IACtD;AAGA,SAAK,8BAA8B,aAAa;AAAA,EAClD;AAAA;AAAA,EAGA,kBAAkB,eAAuB,OAAoB;AAC3D,UAAM,WAAW,KAAK,2BAA2B,IAAI,aAAa;AAClE,QAAI,UAAU;AACZ,eAAS,OAAO,KAAK;AACrB,WAAK,2BAA2B,OAAO,aAAa;AAAA,IACtD;AAGA,SAAK,8BAA8B,eAAe,IAAI;AAAA,EACxD;AAAA;AAAA,EAGA,+BACE,sBACA,wBACM;AACN,SAAK,wBAAwB;AAAA,MAC3B;AAAA,MACA;AAAA,IAAA;AAAA,EAEJ;AAAA,EAEQ,8BACN,eACA,iBAAiB,OACX;AACN,UAAM,gBAAgB,KAAK,wBAAwB,IAAI,aAAa;AACpE,QAAI,CAAC,eAAe;AAClB;AAAA,IACF;AAEA,SAAK,wBAAwB,OAAO,aAAa;AAEjD,QAAI,gBAAgB;AAClB,oBAAc,SAAA;AACd;AAAA,IACF;AAIA,kBAAc,SAAS,WAAW;AAGlC,UAAM,yCAAyB,IAAA;AAC/B,eAAW,YAAY,cAAc,WAAW;AAG9C,UAAI,CAAC,SAAS,YAAY;AACxB;AAAA,MACF;AACA,YAAM,eAAe,SAAS,WAAW;AACzC,UAAI,mBAAmB,IAAI,YAAY,GAAG;AACxC;AAAA,MACF;AACA,yBAAmB,IAAI,YAAY;AACnC,eAAS,WAAW,OAAO,aAAa,OAAO,cAAc,EAAE;AAC/D,eAAS,WAAW,OAAO,yBAAyB,KAAK;AAAA,IAC3D;AAAA,EACF;AAAA,EAEA,MAAM,iBAAiB,IAA2B;AAChD,QAAI,CAAC,KAAK,QAAQ;AAChB;AAAA,IACF;AACA,UAAM,KAAK,OAAO,OAAO,EAAE;AAAA,EAC7B;AAAA,EAEA,MAAM,aAAiD;AACrD,QAAI,CAAC,KAAK,QAAQ;AAChB,aAAO,CAAA;AAAA,IACT;AACA,WAAO,KAAK,OAAO,OAAA;AAAA,EACrB;AAAA,EAEA,MAAM,cAA6B;AACjC,QAAI,CAAC,KAAK,UAAU,CAAC,KAAK,UAAU;AAClC;AAAA,IACF;AACA,UAAM,KAAK,OAAO,MAAA;AAClB,SAAK,SAAS,MAAA;AAAA,EAChB;AAAA,EAEA,kBAA0B;AACxB,QAAI,CAAC,KAAK,UAAU;AAClB,aAAO;AAAA,IACT;AACA,WAAO,KAAK,SAAS,gBAAA;AAAA,EACvB;AAAA,EAEA,kBAA0B;AACxB,QAAI,CAAC,KAAK,UAAU;AAClB,aAAO;AAAA,IACT;AACA,WAAO,KAAK,SAAS,gBAAA;AAAA,EACvB;AAAA,EAEA,oBAAoC;AAClC,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,WAAoB;AAClB,WAAO,KAAK,eAAe,SAAA;AAAA,EAC7B;AAAA,EAEA,UAAgB;AACd,SAAK,WAAW;AAChB,SAAK,UAAU,MAAA;AACf,eAAW,cAAc,OAAO,OAAO,KAAK,OAAO,WAAW,GAAG;AAC/D,iBAAW,mBAAmB;AAAA,IAChC;AAEA,QAAI,KAAK,mBAAmB;AAC1B,WAAK,kBAAA;AACL,WAAK,oBAAoB;AAAA,IAC3B;AAEA,QAAI,KAAK,uBAAuB;AAC9B,WAAK,sBAAA;AACL,WAAK,wBAAwB;AAAA,IAC/B;AAEA,QAAI,KAAK,gBAAgB;AACvB,WAAK,eAAe,kBAAA;AAEpB,UAAI,aAAa,KAAK,gBAAgB;AAClC,aAAK,eAAuB,QAAA;AAAA,MAChC;AAAA,IACF;AAEA,SAAK,eAAe,QAAA;AAAA,EACtB;AACF;AAEO,SAAS,qBAAqB,QAAwC;AAC3E,SAAO,IAAIC,kBAAgB,MAAM;AACnC;;;"}
@@ -10,6 +10,7 @@ export declare class OfflineExecutor {
10
10
  private leaderElection;
11
11
  private onlineDetector;
12
12
  private isLeaderState;
13
+ private disposed;
13
14
  private unsubscribeOnline;
14
15
  private unsubscribeLeadership;
15
16
  readonly mode: OfflineMode;
@@ -36,7 +36,10 @@ class OfflineTransaction {
36
36
  this.offlineId
37
37
  );
38
38
  try {
39
- await this.persistTransaction(offlineTransaction);
39
+ await Promise.race([
40
+ this.persistTransaction(offlineTransaction),
41
+ completionPromise
42
+ ]);
40
43
  await completionPromise;
41
44
  } catch (error) {
42
45
  const normalizedError = error instanceof Error ? error : new Error(String(error));
@@ -1 +1 @@
1
- {"version":3,"file":"OfflineTransaction.cjs","sources":["../../../src/api/OfflineTransaction.ts"],"sourcesContent":["import { createTransaction, safeRandomUUID } from '@tanstack/db'\nimport { NonRetriableError } from '../types'\nimport type { PendingMutation, Transaction } from '@tanstack/db'\nimport type {\n CreateOfflineTransactionOptions,\n OfflineMutationFn,\n OfflineTransaction as OfflineTransactionType,\n} from '../types'\n\nexport class OfflineTransaction {\n private offlineId: string\n private mutationFnName: string\n private autoCommit: boolean\n private idempotencyKey: string\n private metadata: Record<string, any>\n private transaction: Transaction | null = null\n private persistTransaction: (tx: OfflineTransactionType) => Promise<void>\n private executor: any // Will be typed properly - reference to OfflineExecutor\n\n constructor(\n options: CreateOfflineTransactionOptions,\n mutationFn: OfflineMutationFn,\n persistTransaction: (tx: OfflineTransactionType) => Promise<void>,\n executor: any,\n ) {\n this.offlineId = safeRandomUUID()\n this.mutationFnName = options.mutationFnName\n this.autoCommit = options.autoCommit ?? true\n this.idempotencyKey = options.idempotencyKey ?? safeRandomUUID()\n this.metadata = options.metadata ?? {}\n this.persistTransaction = persistTransaction\n this.executor = executor\n }\n\n mutate(callback: () => void): Transaction {\n this.transaction = createTransaction({\n id: this.offlineId,\n autoCommit: false,\n mutationFn: async () => {\n // This is the blocking mutationFn that waits for the executor\n // First persist the transaction to the outbox\n const offlineTransaction: OfflineTransactionType = {\n id: this.offlineId,\n mutationFnName: this.mutationFnName,\n mutations: this.transaction!.mutations,\n keys: this.extractKeys(this.transaction!.mutations),\n idempotencyKey: this.idempotencyKey,\n createdAt: new Date(),\n retryCount: 0,\n nextAttemptAt: Date.now(),\n metadata: this.metadata,\n spanContext: undefined,\n version: 1,\n }\n\n const completionPromise = this.executor.waitForTransactionCompletion(\n this.offlineId,\n )\n\n try {\n await this.persistTransaction(offlineTransaction)\n // Now block and wait for the executor to complete the real mutation\n await completionPromise\n } catch (error) {\n const normalizedError =\n error instanceof Error ? error : new Error(String(error))\n this.executor.rejectTransaction(this.offlineId, normalizedError)\n throw error\n }\n\n return\n },\n metadata: this.metadata,\n })\n\n this.transaction.mutate(() => {\n callback()\n })\n\n if (this.autoCommit) {\n // Auto-commit for direct OfflineTransaction usage\n this.commit().catch((error) => {\n console.error(`Auto-commit failed:`, error)\n throw error\n })\n }\n\n return this.transaction\n }\n\n async commit(): Promise<Transaction> {\n if (!this.transaction) {\n throw new Error(`No mutations to commit. Call mutate() first.`)\n }\n\n try {\n // Commit the TanStack DB transaction\n // This will trigger the mutationFn which handles persistence and waiting\n await this.transaction.commit()\n return this.transaction\n } catch (error) {\n // Only rollback for NonRetriableError - other errors should allow retry\n if (error instanceof NonRetriableError) {\n this.transaction.rollback()\n }\n throw error\n }\n }\n\n rollback(): void {\n if (this.transaction) {\n this.transaction.rollback()\n }\n }\n\n private extractKeys(mutations: Array<PendingMutation>): Array<string> {\n return mutations.map((mutation) => mutation.globalKey)\n }\n\n get id(): string {\n return this.offlineId\n }\n}\n"],"names":["safeRandomUUID","createTransaction","NonRetriableError"],"mappings":";;;;AASO,MAAM,mBAAmB;AAAA;AAAA,EAU9B,YACE,SACA,YACA,oBACA,UACA;AATF,SAAQ,cAAkC;AAUxC,SAAK,YAAYA,kBAAA;AACjB,SAAK,iBAAiB,QAAQ;AAC9B,SAAK,aAAa,QAAQ,cAAc;AACxC,SAAK,iBAAiB,QAAQ,kBAAkBA,GAAAA,eAAA;AAChD,SAAK,WAAW,QAAQ,YAAY,CAAA;AACpC,SAAK,qBAAqB;AAC1B,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,OAAO,UAAmC;AACxC,SAAK,cAAcC,qBAAkB;AAAA,MACnC,IAAI,KAAK;AAAA,MACT,YAAY;AAAA,MACZ,YAAY,YAAY;AAGtB,cAAM,qBAA6C;AAAA,UACjD,IAAI,KAAK;AAAA,UACT,gBAAgB,KAAK;AAAA,UACrB,WAAW,KAAK,YAAa;AAAA,UAC7B,MAAM,KAAK,YAAY,KAAK,YAAa,SAAS;AAAA,UAClD,gBAAgB,KAAK;AAAA,UACrB,+BAAe,KAAA;AAAA,UACf,YAAY;AAAA,UACZ,eAAe,KAAK,IAAA;AAAA,UACpB,UAAU,KAAK;AAAA,UACf,aAAa;AAAA,UACb,SAAS;AAAA,QAAA;AAGX,cAAM,oBAAoB,KAAK,SAAS;AAAA,UACtC,KAAK;AAAA,QAAA;AAGP,YAAI;AACF,gBAAM,KAAK,mBAAmB,kBAAkB;AAEhD,gBAAM;AAAA,QACR,SAAS,OAAO;AACd,gBAAM,kBACJ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAC1D,eAAK,SAAS,kBAAkB,KAAK,WAAW,eAAe;AAC/D,gBAAM;AAAA,QACR;AAEA;AAAA,MACF;AAAA,MACA,UAAU,KAAK;AAAA,IAAA,CAChB;AAED,SAAK,YAAY,OAAO,MAAM;AAC5B,eAAA;AAAA,IACF,CAAC;AAED,QAAI,KAAK,YAAY;AAEnB,WAAK,OAAA,EAAS,MAAM,CAAC,UAAU;AAC7B,gBAAQ,MAAM,uBAAuB,KAAK;AAC1C,cAAM;AAAA,MACR,CAAC;AAAA,IACH;AAEA,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAM,SAA+B;AACnC,QAAI,CAAC,KAAK,aAAa;AACrB,YAAM,IAAI,MAAM,8CAA8C;AAAA,IAChE;AAEA,QAAI;AAGF,YAAM,KAAK,YAAY,OAAA;AACvB,aAAO,KAAK;AAAA,IACd,SAAS,OAAO;AAEd,UAAI,iBAAiBC,MAAAA,mBAAmB;AACtC,aAAK,YAAY,SAAA;AAAA,MACnB;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,WAAiB;AACf,QAAI,KAAK,aAAa;AACpB,WAAK,YAAY,SAAA;AAAA,IACnB;AAAA,EACF;AAAA,EAEQ,YAAY,WAAkD;AACpE,WAAO,UAAU,IAAI,CAAC,aAAa,SAAS,SAAS;AAAA,EACvD;AAAA,EAEA,IAAI,KAAa;AACf,WAAO,KAAK;AAAA,EACd;AACF;;"}
1
+ {"version":3,"file":"OfflineTransaction.cjs","sources":["../../../src/api/OfflineTransaction.ts"],"sourcesContent":["import { createTransaction, safeRandomUUID } from '@tanstack/db'\nimport { NonRetriableError } from '../types'\nimport type { PendingMutation, Transaction } from '@tanstack/db'\nimport type {\n CreateOfflineTransactionOptions,\n OfflineMutationFn,\n OfflineTransaction as OfflineTransactionType,\n} from '../types'\n\nexport class OfflineTransaction {\n private offlineId: string\n private mutationFnName: string\n private autoCommit: boolean\n private idempotencyKey: string\n private metadata: Record<string, any>\n private transaction: Transaction | null = null\n private persistTransaction: (tx: OfflineTransactionType) => Promise<void>\n private executor: any // Will be typed properly - reference to OfflineExecutor\n\n constructor(\n options: CreateOfflineTransactionOptions,\n mutationFn: OfflineMutationFn,\n persistTransaction: (tx: OfflineTransactionType) => Promise<void>,\n executor: any,\n ) {\n this.offlineId = safeRandomUUID()\n this.mutationFnName = options.mutationFnName\n this.autoCommit = options.autoCommit ?? true\n this.idempotencyKey = options.idempotencyKey ?? safeRandomUUID()\n this.metadata = options.metadata ?? {}\n this.persistTransaction = persistTransaction\n this.executor = executor\n }\n\n mutate(callback: () => void): Transaction {\n this.transaction = createTransaction({\n id: this.offlineId,\n autoCommit: false,\n mutationFn: async () => {\n // This is the blocking mutationFn that waits for the executor\n // First persist the transaction to the outbox\n const offlineTransaction: OfflineTransactionType = {\n id: this.offlineId,\n mutationFnName: this.mutationFnName,\n mutations: this.transaction!.mutations,\n keys: this.extractKeys(this.transaction!.mutations),\n idempotencyKey: this.idempotencyKey,\n createdAt: new Date(),\n retryCount: 0,\n nextAttemptAt: Date.now(),\n metadata: this.metadata,\n spanContext: undefined,\n version: 1,\n }\n\n const completionPromise = this.executor.waitForTransactionCompletion(\n this.offlineId,\n )\n\n try {\n // Persistence also drives the shared queue. This transaction can finish\n // before that queue drains; observe both promises from the outset.\n await Promise.race([\n this.persistTransaction(offlineTransaction),\n completionPromise,\n ])\n // A queue pause (offline or retry) is not transaction completion.\n await completionPromise\n } catch (error) {\n const normalizedError =\n error instanceof Error ? error : new Error(String(error))\n this.executor.rejectTransaction(this.offlineId, normalizedError)\n throw error\n }\n\n return\n },\n metadata: this.metadata,\n })\n\n this.transaction.mutate(() => {\n callback()\n })\n\n if (this.autoCommit) {\n // Auto-commit for direct OfflineTransaction usage\n this.commit().catch((error) => {\n console.error(`Auto-commit failed:`, error)\n throw error\n })\n }\n\n return this.transaction\n }\n\n async commit(): Promise<Transaction> {\n if (!this.transaction) {\n throw new Error(`No mutations to commit. Call mutate() first.`)\n }\n\n try {\n // Commit the TanStack DB transaction\n // This will trigger the mutationFn which handles persistence and waiting\n await this.transaction.commit()\n return this.transaction\n } catch (error) {\n // Only rollback for NonRetriableError - other errors should allow retry\n if (error instanceof NonRetriableError) {\n this.transaction.rollback()\n }\n throw error\n }\n }\n\n rollback(): void {\n if (this.transaction) {\n this.transaction.rollback()\n }\n }\n\n private extractKeys(mutations: Array<PendingMutation>): Array<string> {\n return mutations.map((mutation) => mutation.globalKey)\n }\n\n get id(): string {\n return this.offlineId\n }\n}\n"],"names":["safeRandomUUID","createTransaction","NonRetriableError"],"mappings":";;;;AASO,MAAM,mBAAmB;AAAA;AAAA,EAU9B,YACE,SACA,YACA,oBACA,UACA;AATF,SAAQ,cAAkC;AAUxC,SAAK,YAAYA,kBAAA;AACjB,SAAK,iBAAiB,QAAQ;AAC9B,SAAK,aAAa,QAAQ,cAAc;AACxC,SAAK,iBAAiB,QAAQ,kBAAkBA,GAAAA,eAAA;AAChD,SAAK,WAAW,QAAQ,YAAY,CAAA;AACpC,SAAK,qBAAqB;AAC1B,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,OAAO,UAAmC;AACxC,SAAK,cAAcC,qBAAkB;AAAA,MACnC,IAAI,KAAK;AAAA,MACT,YAAY;AAAA,MACZ,YAAY,YAAY;AAGtB,cAAM,qBAA6C;AAAA,UACjD,IAAI,KAAK;AAAA,UACT,gBAAgB,KAAK;AAAA,UACrB,WAAW,KAAK,YAAa;AAAA,UAC7B,MAAM,KAAK,YAAY,KAAK,YAAa,SAAS;AAAA,UAClD,gBAAgB,KAAK;AAAA,UACrB,+BAAe,KAAA;AAAA,UACf,YAAY;AAAA,UACZ,eAAe,KAAK,IAAA;AAAA,UACpB,UAAU,KAAK;AAAA,UACf,aAAa;AAAA,UACb,SAAS;AAAA,QAAA;AAGX,cAAM,oBAAoB,KAAK,SAAS;AAAA,UACtC,KAAK;AAAA,QAAA;AAGP,YAAI;AAGF,gBAAM,QAAQ,KAAK;AAAA,YACjB,KAAK,mBAAmB,kBAAkB;AAAA,YAC1C;AAAA,UAAA,CACD;AAED,gBAAM;AAAA,QACR,SAAS,OAAO;AACd,gBAAM,kBACJ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAC1D,eAAK,SAAS,kBAAkB,KAAK,WAAW,eAAe;AAC/D,gBAAM;AAAA,QACR;AAEA;AAAA,MACF;AAAA,MACA,UAAU,KAAK;AAAA,IAAA,CAChB;AAED,SAAK,YAAY,OAAO,MAAM;AAC5B,eAAA;AAAA,IACF,CAAC;AAED,QAAI,KAAK,YAAY;AAEnB,WAAK,OAAA,EAAS,MAAM,CAAC,UAAU;AAC7B,gBAAQ,MAAM,uBAAuB,KAAK;AAC1C,cAAM;AAAA,MACR,CAAC;AAAA,IACH;AAEA,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAM,SAA+B;AACnC,QAAI,CAAC,KAAK,aAAa;AACrB,YAAM,IAAI,MAAM,8CAA8C;AAAA,IAChE;AAEA,QAAI;AAGF,YAAM,KAAK,YAAY,OAAA;AACvB,aAAO,KAAK;AAAA,IACd,SAAS,OAAO;AAEd,UAAI,iBAAiBC,MAAAA,mBAAmB;AACtC,aAAK,YAAY,SAAA;AAAA,MACnB;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,WAAiB;AACf,QAAI,KAAK,aAAa;AACpB,WAAK,YAAY,SAAA;AAAA,IACnB;AAAA,EACF;AAAA,EAEQ,YAAY,WAAkD;AACpE,WAAO,UAAU,IAAI,CAAC,aAAa,SAAS,SAAS;AAAA,EACvD;AAAA,EAEA,IAAI,KAAa;AACf,WAAO,KAAK;AAAA,EACd;AACF;;"}
@@ -60,7 +60,7 @@ class WebOnlineDetector {
60
60
  }
61
61
  isOnline() {
62
62
  if (typeof navigator !== `undefined`) {
63
- return navigator.onLine;
63
+ return navigator.onLine || typeof document !== `undefined` && document.visibilityState === `visible`;
64
64
  }
65
65
  return true;
66
66
  }
@@ -1 +1 @@
1
- {"version":3,"file":"OnlineDetector.cjs","sources":["../../../src/connectivity/OnlineDetector.ts"],"sourcesContent":["import type { OnlineDetector } from '../types'\n\n/**\n * Web-based online detector that uses browser APIs.\n * Listens for:\n * - `window.online` event for network connectivity changes\n * - `document.visibilitychange` event for tab/window focus changes\n */\nexport class WebOnlineDetector implements OnlineDetector {\n private listeners: Set<() => void> = new Set()\n private isListening = false\n\n constructor() {\n this.startListening()\n }\n\n private startListening(): void {\n if (this.isListening) {\n return\n }\n\n this.isListening = true\n\n if (typeof window !== `undefined`) {\n window.addEventListener(`online`, this.handleOnline)\n document.addEventListener(`visibilitychange`, this.handleVisibilityChange)\n }\n }\n\n private stopListening(): void {\n if (!this.isListening) {\n return\n }\n\n this.isListening = false\n\n if (typeof window !== `undefined`) {\n window.removeEventListener(`online`, this.handleOnline)\n document.removeEventListener(\n `visibilitychange`,\n this.handleVisibilityChange,\n )\n }\n }\n\n private handleOnline = (): void => {\n this.notifyListeners()\n }\n\n private handleVisibilityChange = (): void => {\n if (document.visibilityState === `visible`) {\n this.notifyListeners()\n }\n }\n\n private notifyListeners(): void {\n for (const listener of this.listeners) {\n try {\n listener()\n } catch (error) {\n console.warn(`OnlineDetector listener error:`, error)\n }\n }\n }\n\n subscribe(callback: () => void): () => void {\n this.listeners.add(callback)\n\n return () => {\n this.listeners.delete(callback)\n\n if (this.listeners.size === 0) {\n this.stopListening()\n }\n }\n }\n\n notifyOnline(): void {\n this.notifyListeners()\n }\n\n isOnline(): boolean {\n if (typeof navigator !== `undefined`) {\n return navigator.onLine\n }\n return true\n }\n\n dispose(): void {\n this.stopListening()\n this.listeners.clear()\n }\n}\n\n/**\n * @deprecated Use `WebOnlineDetector` instead. This alias is kept for backwards compatibility.\n */\nexport const DefaultOnlineDetector = WebOnlineDetector\n"],"names":[],"mappings":";;AAQO,MAAM,kBAA4C;AAAA,EAIvD,cAAc;AAHd,SAAQ,gCAAiC,IAAA;AACzC,SAAQ,cAAc;AAmCtB,SAAQ,eAAe,MAAY;AACjC,WAAK,gBAAA;AAAA,IACP;AAEA,SAAQ,yBAAyB,MAAY;AAC3C,UAAI,SAAS,oBAAoB,WAAW;AAC1C,aAAK,gBAAA;AAAA,MACP;AAAA,IACF;AAxCE,SAAK,eAAA;AAAA,EACP;AAAA,EAEQ,iBAAuB;AAC7B,QAAI,KAAK,aAAa;AACpB;AAAA,IACF;AAEA,SAAK,cAAc;AAEnB,QAAI,OAAO,WAAW,aAAa;AACjC,aAAO,iBAAiB,UAAU,KAAK,YAAY;AACnD,eAAS,iBAAiB,oBAAoB,KAAK,sBAAsB;AAAA,IAC3E;AAAA,EACF;AAAA,EAEQ,gBAAsB;AAC5B,QAAI,CAAC,KAAK,aAAa;AACrB;AAAA,IACF;AAEA,SAAK,cAAc;AAEnB,QAAI,OAAO,WAAW,aAAa;AACjC,aAAO,oBAAoB,UAAU,KAAK,YAAY;AACtD,eAAS;AAAA,QACP;AAAA,QACA,KAAK;AAAA,MAAA;AAAA,IAET;AAAA,EACF;AAAA,EAYQ,kBAAwB;AAC9B,eAAW,YAAY,KAAK,WAAW;AACrC,UAAI;AACF,iBAAA;AAAA,MACF,SAAS,OAAO;AACd,gBAAQ,KAAK,kCAAkC,KAAK;AAAA,MACtD;AAAA,IACF;AAAA,EACF;AAAA,EAEA,UAAU,UAAkC;AAC1C,SAAK,UAAU,IAAI,QAAQ;AAE3B,WAAO,MAAM;AACX,WAAK,UAAU,OAAO,QAAQ;AAE9B,UAAI,KAAK,UAAU,SAAS,GAAG;AAC7B,aAAK,cAAA;AAAA,MACP;AAAA,IACF;AAAA,EACF;AAAA,EAEA,eAAqB;AACnB,SAAK,gBAAA;AAAA,EACP;AAAA,EAEA,WAAoB;AAClB,QAAI,OAAO,cAAc,aAAa;AACpC,aAAO,UAAU;AAAA,IACnB;AACA,WAAO;AAAA,EACT;AAAA,EAEA,UAAgB;AACd,SAAK,cAAA;AACL,SAAK,UAAU,MAAA;AAAA,EACjB;AACF;AAKO,MAAM,wBAAwB;;;"}
1
+ {"version":3,"file":"OnlineDetector.cjs","sources":["../../../src/connectivity/OnlineDetector.ts"],"sourcesContent":["import type { OnlineDetector } from '../types'\n\n/**\n * Web-based online detector that uses browser APIs.\n * Listens for:\n * - `window.online` event for network connectivity changes\n * - `document.visibilitychange` event for tab/window focus changes\n */\nexport class WebOnlineDetector implements OnlineDetector {\n private listeners: Set<() => void> = new Set()\n private isListening = false\n\n constructor() {\n this.startListening()\n }\n\n private startListening(): void {\n if (this.isListening) {\n return\n }\n\n this.isListening = true\n\n if (typeof window !== `undefined`) {\n window.addEventListener(`online`, this.handleOnline)\n document.addEventListener(`visibilitychange`, this.handleVisibilityChange)\n }\n }\n\n private stopListening(): void {\n if (!this.isListening) {\n return\n }\n\n this.isListening = false\n\n if (typeof window !== `undefined`) {\n window.removeEventListener(`online`, this.handleOnline)\n document.removeEventListener(\n `visibilitychange`,\n this.handleVisibilityChange,\n )\n }\n }\n\n private handleOnline = (): void => {\n this.notifyListeners()\n }\n\n private handleVisibilityChange = (): void => {\n if (document.visibilityState === `visible`) {\n this.notifyListeners()\n }\n }\n\n private notifyListeners(): void {\n for (const listener of this.listeners) {\n try {\n listener()\n } catch (error) {\n console.warn(`OnlineDetector listener error:`, error)\n }\n }\n }\n\n subscribe(callback: () => void): () => void {\n this.listeners.add(callback)\n\n return () => {\n this.listeners.delete(callback)\n\n if (this.listeners.size === 0) {\n this.stopListening()\n }\n }\n }\n\n notifyOnline(): void {\n this.notifyListeners()\n }\n\n isOnline(): boolean {\n if (typeof navigator !== `undefined`) {\n // A visible tab may reach the server despite a stale browser hint.\n return (\n navigator.onLine ||\n (typeof document !== `undefined` &&\n document.visibilityState === `visible`)\n )\n }\n return true\n }\n\n dispose(): void {\n this.stopListening()\n this.listeners.clear()\n }\n}\n\n/**\n * @deprecated Use `WebOnlineDetector` instead. This alias is kept for backwards compatibility.\n */\nexport const DefaultOnlineDetector = WebOnlineDetector\n"],"names":[],"mappings":";;AAQO,MAAM,kBAA4C;AAAA,EAIvD,cAAc;AAHd,SAAQ,gCAAiC,IAAA;AACzC,SAAQ,cAAc;AAmCtB,SAAQ,eAAe,MAAY;AACjC,WAAK,gBAAA;AAAA,IACP;AAEA,SAAQ,yBAAyB,MAAY;AAC3C,UAAI,SAAS,oBAAoB,WAAW;AAC1C,aAAK,gBAAA;AAAA,MACP;AAAA,IACF;AAxCE,SAAK,eAAA;AAAA,EACP;AAAA,EAEQ,iBAAuB;AAC7B,QAAI,KAAK,aAAa;AACpB;AAAA,IACF;AAEA,SAAK,cAAc;AAEnB,QAAI,OAAO,WAAW,aAAa;AACjC,aAAO,iBAAiB,UAAU,KAAK,YAAY;AACnD,eAAS,iBAAiB,oBAAoB,KAAK,sBAAsB;AAAA,IAC3E;AAAA,EACF;AAAA,EAEQ,gBAAsB;AAC5B,QAAI,CAAC,KAAK,aAAa;AACrB;AAAA,IACF;AAEA,SAAK,cAAc;AAEnB,QAAI,OAAO,WAAW,aAAa;AACjC,aAAO,oBAAoB,UAAU,KAAK,YAAY;AACtD,eAAS;AAAA,QACP;AAAA,QACA,KAAK;AAAA,MAAA;AAAA,IAET;AAAA,EACF;AAAA,EAYQ,kBAAwB;AAC9B,eAAW,YAAY,KAAK,WAAW;AACrC,UAAI;AACF,iBAAA;AAAA,MACF,SAAS,OAAO;AACd,gBAAQ,KAAK,kCAAkC,KAAK;AAAA,MACtD;AAAA,IACF;AAAA,EACF;AAAA,EAEA,UAAU,UAAkC;AAC1C,SAAK,UAAU,IAAI,QAAQ;AAE3B,WAAO,MAAM;AACX,WAAK,UAAU,OAAO,QAAQ;AAE9B,UAAI,KAAK,UAAU,SAAS,GAAG;AAC7B,aAAK,cAAA;AAAA,MACP;AAAA,IACF;AAAA,EACF;AAAA,EAEA,eAAqB;AACnB,SAAK,gBAAA;AAAA,EACP;AAAA,EAEA,WAAoB;AAClB,QAAI,OAAO,cAAc,aAAa;AAEpC,aACE,UAAU,UACT,OAAO,aAAa,eACnB,SAAS,oBAAoB;AAAA,IAEnC;AACA,WAAO;AAAA,EACT;AAAA,EAEA,UAAgB;AACd,SAAK,cAAA;AACL,SAAK,UAAU,MAAA;AAAA,EACjB;AACF;AAKO,MAAM,wBAAwB;;;"}
@@ -8,7 +8,7 @@ class ReactNativeOnlineDetector {
8
8
  this.netInfoUnsubscribe = null;
9
9
  this.appStateSubscription = null;
10
10
  this.isListening = false;
11
- this.wasConnected = true;
11
+ this.wasConnected = false;
12
12
  this.handleAppStateChange = (nextState) => {
13
13
  if (nextState === `active`) {
14
14
  this.notifyListeners();
@@ -21,12 +21,6 @@ class ReactNativeOnlineDetector {
21
21
  return;
22
22
  }
23
23
  this.isListening = true;
24
- if (typeof NetInfo.fetch === `function`) {
25
- void NetInfo.fetch().then((state) => {
26
- this.wasConnected = this.toConnectivityState(state);
27
- }).catch(() => {
28
- });
29
- }
30
24
  this.netInfoUnsubscribe = NetInfo.addEventListener((state) => {
31
25
  const isConnected = this.toConnectivityState(state);
32
26
  if (isConnected && !this.wasConnected) {
@@ -1 +1 @@
1
- {"version":3,"file":"ReactNativeOnlineDetector.cjs","sources":["../../../src/connectivity/ReactNativeOnlineDetector.ts"],"sourcesContent":["import NetInfo from '@react-native-community/netinfo'\nimport { AppState } from 'react-native'\nimport type { AppStateStatus, NativeEventSubscription } from 'react-native'\nimport type { OnlineDetector } from '../types'\n\n/**\n * React Native online detector that uses RN APIs.\n * Listens for:\n * - Network connectivity changes via `@react-native-community/netinfo`\n * - App state changes (foreground/background) via `AppState`\n */\nexport class ReactNativeOnlineDetector implements OnlineDetector {\n private listeners: Set<() => void> = new Set()\n private netInfoUnsubscribe: (() => void) | null = null\n private appStateSubscription: NativeEventSubscription | null = null\n private isListening = false\n private wasConnected = true\n\n constructor() {\n this.startListening()\n }\n\n private startListening(): void {\n if (this.isListening) {\n return\n }\n\n this.isListening = true\n\n if (typeof NetInfo.fetch === `function`) {\n void NetInfo.fetch()\n .then((state) => {\n this.wasConnected = this.toConnectivityState(state)\n })\n .catch(() => {\n // Ignore initial fetch failures and rely on subscription updates.\n })\n }\n\n // Subscribe to network state changes\n this.netInfoUnsubscribe = NetInfo.addEventListener((state) => {\n const isConnected = this.toConnectivityState(state)\n\n // Only notify when transitioning to online\n if (isConnected && !this.wasConnected) {\n this.notifyListeners()\n }\n\n this.wasConnected = isConnected\n })\n\n // Subscribe to app state changes (foreground/background)\n this.appStateSubscription = AppState.addEventListener(\n `change`,\n this.handleAppStateChange,\n )\n }\n\n private handleAppStateChange = (nextState: AppStateStatus): void => {\n // Notify when app becomes active (foreground)\n if (nextState === `active`) {\n this.notifyListeners()\n }\n }\n\n private stopListening(): void {\n if (!this.isListening) {\n return\n }\n\n this.isListening = false\n\n if (this.netInfoUnsubscribe) {\n this.netInfoUnsubscribe()\n this.netInfoUnsubscribe = null\n }\n\n if (this.appStateSubscription) {\n this.appStateSubscription.remove()\n this.appStateSubscription = null\n }\n }\n\n private notifyListeners(): void {\n for (const listener of this.listeners) {\n try {\n listener()\n } catch (error) {\n console.warn(`ReactNativeOnlineDetector listener error:`, error)\n }\n }\n }\n\n subscribe(callback: () => void): () => void {\n this.listeners.add(callback)\n\n return () => {\n this.listeners.delete(callback)\n\n if (this.listeners.size === 0) {\n this.stopListening()\n }\n }\n }\n\n notifyOnline(): void {\n this.notifyListeners()\n }\n\n isOnline(): boolean {\n return this.wasConnected\n }\n\n dispose(): void {\n this.stopListening()\n this.listeners.clear()\n }\n\n private toConnectivityState(state: {\n isConnected: boolean | null\n isInternetReachable: boolean | null\n }): boolean {\n return !!state.isConnected && state.isInternetReachable !== false\n }\n}\n"],"names":["AppState"],"mappings":";;;;AAWO,MAAM,0BAAoD;AAAA,EAO/D,cAAc;AANd,SAAQ,gCAAiC,IAAA;AACzC,SAAQ,qBAA0C;AAClD,SAAQ,uBAAuD;AAC/D,SAAQ,cAAc;AACtB,SAAQ,eAAe;AA0CvB,SAAQ,uBAAuB,CAAC,cAAoC;AAElE,UAAI,cAAc,UAAU;AAC1B,aAAK,gBAAA;AAAA,MACP;AAAA,IACF;AA5CE,SAAK,eAAA;AAAA,EACP;AAAA,EAEQ,iBAAuB;AAC7B,QAAI,KAAK,aAAa;AACpB;AAAA,IACF;AAEA,SAAK,cAAc;AAEnB,QAAI,OAAO,QAAQ,UAAU,YAAY;AACvC,WAAK,QAAQ,MAAA,EACV,KAAK,CAAC,UAAU;AACf,aAAK,eAAe,KAAK,oBAAoB,KAAK;AAAA,MACpD,CAAC,EACA,MAAM,MAAM;AAAA,MAEb,CAAC;AAAA,IACL;AAGA,SAAK,qBAAqB,QAAQ,iBAAiB,CAAC,UAAU;AAC5D,YAAM,cAAc,KAAK,oBAAoB,KAAK;AAGlD,UAAI,eAAe,CAAC,KAAK,cAAc;AACrC,aAAK,gBAAA;AAAA,MACP;AAEA,WAAK,eAAe;AAAA,IACtB,CAAC;AAGD,SAAK,uBAAuBA,YAAAA,SAAS;AAAA,MACnC;AAAA,MACA,KAAK;AAAA,IAAA;AAAA,EAET;AAAA,EASQ,gBAAsB;AAC5B,QAAI,CAAC,KAAK,aAAa;AACrB;AAAA,IACF;AAEA,SAAK,cAAc;AAEnB,QAAI,KAAK,oBAAoB;AAC3B,WAAK,mBAAA;AACL,WAAK,qBAAqB;AAAA,IAC5B;AAEA,QAAI,KAAK,sBAAsB;AAC7B,WAAK,qBAAqB,OAAA;AAC1B,WAAK,uBAAuB;AAAA,IAC9B;AAAA,EACF;AAAA,EAEQ,kBAAwB;AAC9B,eAAW,YAAY,KAAK,WAAW;AACrC,UAAI;AACF,iBAAA;AAAA,MACF,SAAS,OAAO;AACd,gBAAQ,KAAK,6CAA6C,KAAK;AAAA,MACjE;AAAA,IACF;AAAA,EACF;AAAA,EAEA,UAAU,UAAkC;AAC1C,SAAK,UAAU,IAAI,QAAQ;AAE3B,WAAO,MAAM;AACX,WAAK,UAAU,OAAO,QAAQ;AAE9B,UAAI,KAAK,UAAU,SAAS,GAAG;AAC7B,aAAK,cAAA;AAAA,MACP;AAAA,IACF;AAAA,EACF;AAAA,EAEA,eAAqB;AACnB,SAAK,gBAAA;AAAA,EACP;AAAA,EAEA,WAAoB;AAClB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,UAAgB;AACd,SAAK,cAAA;AACL,SAAK,UAAU,MAAA;AAAA,EACjB;AAAA,EAEQ,oBAAoB,OAGhB;AACV,WAAO,CAAC,CAAC,MAAM,eAAe,MAAM,wBAAwB;AAAA,EAC9D;AACF;;"}
1
+ {"version":3,"file":"ReactNativeOnlineDetector.cjs","sources":["../../../src/connectivity/ReactNativeOnlineDetector.ts"],"sourcesContent":["import NetInfo from '@react-native-community/netinfo'\nimport { AppState } from 'react-native'\nimport type { AppStateStatus, NativeEventSubscription } from 'react-native'\nimport type { OnlineDetector } from '../types'\n\n/**\n * React Native online detector that uses RN APIs.\n * Listens for:\n * - Network connectivity changes via `@react-native-community/netinfo`\n * - App state changes (foreground/background) via `AppState`\n */\nexport class ReactNativeOnlineDetector implements OnlineDetector {\n private listeners: Set<() => void> = new Set()\n private netInfoUnsubscribe: (() => void) | null = null\n private appStateSubscription: NativeEventSubscription | null = null\n private isListening = false\n private wasConnected = false\n\n constructor() {\n this.startListening()\n }\n\n private startListening(): void {\n if (this.isListening) {\n return\n }\n\n this.isListening = true\n\n // Subscribe to network state changes\n this.netInfoUnsubscribe = NetInfo.addEventListener((state) => {\n const isConnected = this.toConnectivityState(state)\n\n // Only notify when transitioning to online\n if (isConnected && !this.wasConnected) {\n this.notifyListeners()\n }\n\n this.wasConnected = isConnected\n })\n\n // Subscribe to app state changes (foreground/background)\n this.appStateSubscription = AppState.addEventListener(\n `change`,\n this.handleAppStateChange,\n )\n }\n\n private handleAppStateChange = (nextState: AppStateStatus): void => {\n // Notify when app becomes active (foreground)\n if (nextState === `active`) {\n this.notifyListeners()\n }\n }\n\n private stopListening(): void {\n if (!this.isListening) {\n return\n }\n\n this.isListening = false\n\n if (this.netInfoUnsubscribe) {\n this.netInfoUnsubscribe()\n this.netInfoUnsubscribe = null\n }\n\n if (this.appStateSubscription) {\n this.appStateSubscription.remove()\n this.appStateSubscription = null\n }\n }\n\n private notifyListeners(): void {\n for (const listener of this.listeners) {\n try {\n listener()\n } catch (error) {\n console.warn(`ReactNativeOnlineDetector listener error:`, error)\n }\n }\n }\n\n subscribe(callback: () => void): () => void {\n this.listeners.add(callback)\n\n return () => {\n this.listeners.delete(callback)\n\n if (this.listeners.size === 0) {\n this.stopListening()\n }\n }\n }\n\n notifyOnline(): void {\n this.notifyListeners()\n }\n\n isOnline(): boolean {\n return this.wasConnected\n }\n\n dispose(): void {\n this.stopListening()\n this.listeners.clear()\n }\n\n private toConnectivityState(state: {\n isConnected: boolean | null\n isInternetReachable: boolean | null\n }): boolean {\n return !!state.isConnected && state.isInternetReachable !== false\n }\n}\n"],"names":["AppState"],"mappings":";;;;AAWO,MAAM,0BAAoD;AAAA,EAO/D,cAAc;AANd,SAAQ,gCAAiC,IAAA;AACzC,SAAQ,qBAA0C;AAClD,SAAQ,uBAAuD;AAC/D,SAAQ,cAAc;AACtB,SAAQ,eAAe;AAgCvB,SAAQ,uBAAuB,CAAC,cAAoC;AAElE,UAAI,cAAc,UAAU;AAC1B,aAAK,gBAAA;AAAA,MACP;AAAA,IACF;AAlCE,SAAK,eAAA;AAAA,EACP;AAAA,EAEQ,iBAAuB;AAC7B,QAAI,KAAK,aAAa;AACpB;AAAA,IACF;AAEA,SAAK,cAAc;AAGnB,SAAK,qBAAqB,QAAQ,iBAAiB,CAAC,UAAU;AAC5D,YAAM,cAAc,KAAK,oBAAoB,KAAK;AAGlD,UAAI,eAAe,CAAC,KAAK,cAAc;AACrC,aAAK,gBAAA;AAAA,MACP;AAEA,WAAK,eAAe;AAAA,IACtB,CAAC;AAGD,SAAK,uBAAuBA,YAAAA,SAAS;AAAA,MACnC;AAAA,MACA,KAAK;AAAA,IAAA;AAAA,EAET;AAAA,EASQ,gBAAsB;AAC5B,QAAI,CAAC,KAAK,aAAa;AACrB;AAAA,IACF;AAEA,SAAK,cAAc;AAEnB,QAAI,KAAK,oBAAoB;AAC3B,WAAK,mBAAA;AACL,WAAK,qBAAqB;AAAA,IAC5B;AAEA,QAAI,KAAK,sBAAsB;AAC7B,WAAK,qBAAqB,OAAA;AAC1B,WAAK,uBAAuB;AAAA,IAC9B;AAAA,EACF;AAAA,EAEQ,kBAAwB;AAC9B,eAAW,YAAY,KAAK,WAAW;AACrC,UAAI;AACF,iBAAA;AAAA,MACF,SAAS,OAAO;AACd,gBAAQ,KAAK,6CAA6C,KAAK;AAAA,MACjE;AAAA,IACF;AAAA,EACF;AAAA,EAEA,UAAU,UAAkC;AAC1C,SAAK,UAAU,IAAI,QAAQ;AAE3B,WAAO,MAAM;AACX,WAAK,UAAU,OAAO,QAAQ;AAE9B,UAAI,KAAK,UAAU,SAAS,GAAG;AAC7B,aAAK,cAAA;AAAA,MACP;AAAA,IACF;AAAA,EACF;AAAA,EAEA,eAAqB;AACnB,SAAK,gBAAA;AAAA,EACP;AAAA,EAEA,WAAoB;AAClB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,UAAgB;AACd,SAAK,cAAA;AACL,SAAK,UAAU,MAAA;AAAA,EACjB;AAAA,EAEQ,oBAAoB,OAGhB;AACV,WAAO,CAAC,CAAC,MAAM,eAAe,MAAM,wBAAwB;AAAA,EAC9D;AACF;;"}