@tanstack/offline-transactions 0.0.0

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 (138) hide show
  1. package/README.md +219 -0
  2. package/dist/cjs/OfflineExecutor.cjs +266 -0
  3. package/dist/cjs/OfflineExecutor.cjs.map +1 -0
  4. package/dist/cjs/OfflineExecutor.d.cts +39 -0
  5. package/dist/cjs/api/OfflineAction.cjs +47 -0
  6. package/dist/cjs/api/OfflineAction.cjs.map +1 -0
  7. package/dist/cjs/api/OfflineAction.d.cts +3 -0
  8. package/dist/cjs/api/OfflineTransaction.cjs +96 -0
  9. package/dist/cjs/api/OfflineTransaction.cjs.map +1 -0
  10. package/dist/cjs/api/OfflineTransaction.d.cts +18 -0
  11. package/dist/cjs/connectivity/OnlineDetector.cjs +73 -0
  12. package/dist/cjs/connectivity/OnlineDetector.cjs.map +1 -0
  13. package/dist/cjs/connectivity/OnlineDetector.d.cts +15 -0
  14. package/dist/cjs/coordination/BroadcastChannelLeader.cjs +146 -0
  15. package/dist/cjs/coordination/BroadcastChannelLeader.cjs.map +1 -0
  16. package/dist/cjs/coordination/BroadcastChannelLeader.d.cts +26 -0
  17. package/dist/cjs/coordination/LeaderElection.cjs +31 -0
  18. package/dist/cjs/coordination/LeaderElection.cjs.map +1 -0
  19. package/dist/cjs/coordination/LeaderElection.d.cts +10 -0
  20. package/dist/cjs/coordination/WebLocksLeader.cjs +71 -0
  21. package/dist/cjs/coordination/WebLocksLeader.cjs.map +1 -0
  22. package/dist/cjs/coordination/WebLocksLeader.d.cts +10 -0
  23. package/dist/cjs/executor/KeyScheduler.cjs +106 -0
  24. package/dist/cjs/executor/KeyScheduler.cjs.map +1 -0
  25. package/dist/cjs/executor/KeyScheduler.d.cts +18 -0
  26. package/dist/cjs/executor/TransactionExecutor.cjs +236 -0
  27. package/dist/cjs/executor/TransactionExecutor.cjs.map +1 -0
  28. package/dist/cjs/executor/TransactionExecutor.d.cts +28 -0
  29. package/dist/cjs/index.cjs +34 -0
  30. package/dist/cjs/index.cjs.map +1 -0
  31. package/dist/cjs/index.d.cts +16 -0
  32. package/dist/cjs/outbox/OutboxManager.cjs +114 -0
  33. package/dist/cjs/outbox/OutboxManager.cjs.map +1 -0
  34. package/dist/cjs/outbox/OutboxManager.d.cts +18 -0
  35. package/dist/cjs/outbox/TransactionSerializer.cjs +135 -0
  36. package/dist/cjs/outbox/TransactionSerializer.cjs.map +1 -0
  37. package/dist/cjs/outbox/TransactionSerializer.d.cts +15 -0
  38. package/dist/cjs/retry/BackoffCalculator.cjs +14 -0
  39. package/dist/cjs/retry/BackoffCalculator.cjs.map +1 -0
  40. package/dist/cjs/retry/BackoffCalculator.d.cts +5 -0
  41. package/dist/cjs/retry/NonRetriableError.d.cts +1 -0
  42. package/dist/cjs/retry/RetryPolicy.cjs +33 -0
  43. package/dist/cjs/retry/RetryPolicy.cjs.map +1 -0
  44. package/dist/cjs/retry/RetryPolicy.d.cts +8 -0
  45. package/dist/cjs/storage/IndexedDBAdapter.cjs +104 -0
  46. package/dist/cjs/storage/IndexedDBAdapter.cjs.map +1 -0
  47. package/dist/cjs/storage/IndexedDBAdapter.d.cts +14 -0
  48. package/dist/cjs/storage/LocalStorageAdapter.cjs +71 -0
  49. package/dist/cjs/storage/LocalStorageAdapter.cjs.map +1 -0
  50. package/dist/cjs/storage/LocalStorageAdapter.d.cts +11 -0
  51. package/dist/cjs/storage/StorageAdapter.cjs +6 -0
  52. package/dist/cjs/storage/StorageAdapter.cjs.map +1 -0
  53. package/dist/cjs/storage/StorageAdapter.d.cts +9 -0
  54. package/dist/cjs/telemetry/tracer.cjs +91 -0
  55. package/dist/cjs/telemetry/tracer.cjs.map +1 -0
  56. package/dist/cjs/telemetry/tracer.d.cts +29 -0
  57. package/dist/cjs/types.cjs +10 -0
  58. package/dist/cjs/types.cjs.map +1 -0
  59. package/dist/cjs/types.d.cts +101 -0
  60. package/dist/esm/OfflineExecutor.d.ts +39 -0
  61. package/dist/esm/OfflineExecutor.js +266 -0
  62. package/dist/esm/OfflineExecutor.js.map +1 -0
  63. package/dist/esm/api/OfflineAction.d.ts +3 -0
  64. package/dist/esm/api/OfflineAction.js +47 -0
  65. package/dist/esm/api/OfflineAction.js.map +1 -0
  66. package/dist/esm/api/OfflineTransaction.d.ts +18 -0
  67. package/dist/esm/api/OfflineTransaction.js +96 -0
  68. package/dist/esm/api/OfflineTransaction.js.map +1 -0
  69. package/dist/esm/connectivity/OnlineDetector.d.ts +15 -0
  70. package/dist/esm/connectivity/OnlineDetector.js +73 -0
  71. package/dist/esm/connectivity/OnlineDetector.js.map +1 -0
  72. package/dist/esm/coordination/BroadcastChannelLeader.d.ts +26 -0
  73. package/dist/esm/coordination/BroadcastChannelLeader.js +146 -0
  74. package/dist/esm/coordination/BroadcastChannelLeader.js.map +1 -0
  75. package/dist/esm/coordination/LeaderElection.d.ts +10 -0
  76. package/dist/esm/coordination/LeaderElection.js +31 -0
  77. package/dist/esm/coordination/LeaderElection.js.map +1 -0
  78. package/dist/esm/coordination/WebLocksLeader.d.ts +10 -0
  79. package/dist/esm/coordination/WebLocksLeader.js +71 -0
  80. package/dist/esm/coordination/WebLocksLeader.js.map +1 -0
  81. package/dist/esm/executor/KeyScheduler.d.ts +18 -0
  82. package/dist/esm/executor/KeyScheduler.js +106 -0
  83. package/dist/esm/executor/KeyScheduler.js.map +1 -0
  84. package/dist/esm/executor/TransactionExecutor.d.ts +28 -0
  85. package/dist/esm/executor/TransactionExecutor.js +236 -0
  86. package/dist/esm/executor/TransactionExecutor.js.map +1 -0
  87. package/dist/esm/index.d.ts +16 -0
  88. package/dist/esm/index.js +34 -0
  89. package/dist/esm/index.js.map +1 -0
  90. package/dist/esm/outbox/OutboxManager.d.ts +18 -0
  91. package/dist/esm/outbox/OutboxManager.js +114 -0
  92. package/dist/esm/outbox/OutboxManager.js.map +1 -0
  93. package/dist/esm/outbox/TransactionSerializer.d.ts +15 -0
  94. package/dist/esm/outbox/TransactionSerializer.js +135 -0
  95. package/dist/esm/outbox/TransactionSerializer.js.map +1 -0
  96. package/dist/esm/retry/BackoffCalculator.d.ts +5 -0
  97. package/dist/esm/retry/BackoffCalculator.js +14 -0
  98. package/dist/esm/retry/BackoffCalculator.js.map +1 -0
  99. package/dist/esm/retry/NonRetriableError.d.ts +1 -0
  100. package/dist/esm/retry/RetryPolicy.d.ts +8 -0
  101. package/dist/esm/retry/RetryPolicy.js +33 -0
  102. package/dist/esm/retry/RetryPolicy.js.map +1 -0
  103. package/dist/esm/storage/IndexedDBAdapter.d.ts +14 -0
  104. package/dist/esm/storage/IndexedDBAdapter.js +104 -0
  105. package/dist/esm/storage/IndexedDBAdapter.js.map +1 -0
  106. package/dist/esm/storage/LocalStorageAdapter.d.ts +11 -0
  107. package/dist/esm/storage/LocalStorageAdapter.js +71 -0
  108. package/dist/esm/storage/LocalStorageAdapter.js.map +1 -0
  109. package/dist/esm/storage/StorageAdapter.d.ts +9 -0
  110. package/dist/esm/storage/StorageAdapter.js +6 -0
  111. package/dist/esm/storage/StorageAdapter.js.map +1 -0
  112. package/dist/esm/telemetry/tracer.d.ts +29 -0
  113. package/dist/esm/telemetry/tracer.js +91 -0
  114. package/dist/esm/telemetry/tracer.js.map +1 -0
  115. package/dist/esm/types.d.ts +101 -0
  116. package/dist/esm/types.js +10 -0
  117. package/dist/esm/types.js.map +1 -0
  118. package/package.json +66 -0
  119. package/src/OfflineExecutor.ts +360 -0
  120. package/src/api/OfflineAction.ts +68 -0
  121. package/src/api/OfflineTransaction.ts +134 -0
  122. package/src/connectivity/OnlineDetector.ts +87 -0
  123. package/src/coordination/BroadcastChannelLeader.ts +181 -0
  124. package/src/coordination/LeaderElection.ts +35 -0
  125. package/src/coordination/WebLocksLeader.ts +82 -0
  126. package/src/executor/KeyScheduler.ts +123 -0
  127. package/src/executor/TransactionExecutor.ts +330 -0
  128. package/src/index.ts +47 -0
  129. package/src/outbox/OutboxManager.ts +141 -0
  130. package/src/outbox/TransactionSerializer.ts +163 -0
  131. package/src/retry/BackoffCalculator.ts +13 -0
  132. package/src/retry/NonRetriableError.ts +1 -0
  133. package/src/retry/RetryPolicy.ts +41 -0
  134. package/src/storage/IndexedDBAdapter.ts +119 -0
  135. package/src/storage/LocalStorageAdapter.ts +79 -0
  136. package/src/storage/StorageAdapter.ts +11 -0
  137. package/src/telemetry/tracer.ts +156 -0
  138. package/src/types.ts +133 -0
package/README.md ADDED
@@ -0,0 +1,219 @@
1
+ # @tanstack/offline-transactions
2
+
3
+ Offline-first transaction capabilities for TanStack DB that provides durable persistence of mutations with automatic retry when connectivity is restored.
4
+
5
+ ## Features
6
+
7
+ - **Outbox Pattern**: Persist mutations before dispatch for zero data loss
8
+ - **Automatic Retry**: Exponential backoff with jitter for failed transactions
9
+ - **Multi-tab Coordination**: Leader election ensures safe storage access
10
+ - **FIFO Sequential Processing**: Transactions execute one at a time in creation order
11
+ - **Flexible Storage**: IndexedDB with localStorage fallback
12
+ - **Type Safe**: Full TypeScript support with TanStack DB integration
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @tanstack/offline-transactions
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```typescript
23
+ import { startOfflineExecutor } from "@tanstack/offline-transactions"
24
+
25
+ // Setup offline executor
26
+ const offline = startOfflineExecutor({
27
+ collections: { todos: todoCollection },
28
+ mutationFns: {
29
+ syncTodos: async ({ transaction, idempotencyKey }) => {
30
+ await api.saveBatch(transaction.mutations, { idempotencyKey })
31
+ },
32
+ },
33
+ onLeadershipChange: (isLeader) => {
34
+ if (!isLeader) {
35
+ console.warn("Running in online-only mode (another tab is the leader)")
36
+ }
37
+ },
38
+ })
39
+
40
+ // Create offline transactions
41
+ const offlineTx = offline.createOfflineTransaction({
42
+ mutationFnName: "syncTodos",
43
+ autoCommit: false,
44
+ })
45
+
46
+ offlineTx.mutate(() => {
47
+ todoCollection.insert({
48
+ id: crypto.randomUUID(),
49
+ text: "Buy milk",
50
+ completed: false,
51
+ })
52
+ })
53
+
54
+ // Execute with automatic offline support
55
+ await offlineTx.commit()
56
+ ```
57
+
58
+ ## Core Concepts
59
+
60
+ ### Outbox-First Persistence
61
+
62
+ Mutations are persisted to a durable outbox before being applied, ensuring zero data loss during offline periods:
63
+
64
+ 1. Mutation is persisted to IndexedDB/localStorage
65
+ 2. Optimistic update is applied locally
66
+ 3. When online, mutation is sent to server
67
+ 4. On success, mutation is removed from outbox
68
+
69
+ ### Multi-tab Coordination
70
+
71
+ Only one tab acts as the "leader" to safely manage the outbox:
72
+
73
+ - **Leader tab**: Full offline support with outbox persistence
74
+ - **Non-leader tabs**: Online-only mode for safety
75
+ - **Leadership transfer**: Automatic failover when leader tab closes
76
+
77
+ ### FIFO Sequential Processing
78
+
79
+ Transactions are processed one at a time in the order they were created:
80
+
81
+ - **Sequential execution**: All transactions execute in FIFO order
82
+ - **Dependency safety**: Avoids conflicts between transactions that may reference each other
83
+ - **Predictable behavior**: Transactions complete in the exact order they were created
84
+
85
+ ## API Reference
86
+
87
+ ### startOfflineExecutor(config)
88
+
89
+ Creates and starts an offline executor instance.
90
+
91
+ ```typescript
92
+ interface OfflineConfig {
93
+ collections: Record<string, Collection>
94
+ mutationFns: Record<string, MutationFn>
95
+ storage?: StorageAdapter
96
+ maxConcurrency?: number
97
+ jitter?: boolean
98
+ beforeRetry?: (transactions: OfflineTransaction[]) => OfflineTransaction[]
99
+ onUnknownMutationFn?: (name: string, tx: OfflineTransaction) => void
100
+ onLeadershipChange?: (isLeader: boolean) => void
101
+ }
102
+ ```
103
+
104
+ ### OfflineExecutor
105
+
106
+ #### Properties
107
+
108
+ - `isOfflineEnabled: boolean` - Whether this tab can persist offline transactions
109
+
110
+ #### Methods
111
+
112
+ - `createOfflineTransaction(options)` - Create a manual offline transaction
113
+ - `waitForTransactionCompletion(id)` - Wait for a specific transaction to complete
114
+ - `removeFromOutbox(id)` - Manually remove transaction from outbox
115
+ - `peekOutbox()` - View all pending transactions
116
+ - `notifyOnline()` - Manually trigger retry execution
117
+ - `dispose()` - Clean up resources
118
+
119
+ ### Error Handling
120
+
121
+ Use `NonRetriableError` for permanent failures:
122
+
123
+ ```typescript
124
+ import { NonRetriableError } from "@tanstack/offline-transactions"
125
+
126
+ const mutationFn = async ({ transaction }) => {
127
+ try {
128
+ await api.save(transaction.mutations)
129
+ } catch (error) {
130
+ if (error.status === 422) {
131
+ throw new NonRetriableError("Invalid data - will not retry")
132
+ }
133
+ throw error // Will retry with backoff
134
+ }
135
+ }
136
+ ```
137
+
138
+ ## Advanced Usage
139
+
140
+ ### Custom Storage Adapter
141
+
142
+ ```typescript
143
+ import {
144
+ IndexedDBAdapter,
145
+ LocalStorageAdapter,
146
+ } from "@tanstack/offline-transactions"
147
+
148
+ const executor = startOfflineExecutor({
149
+ // Use custom storage
150
+ storage: new IndexedDBAdapter("my-app", "transactions"),
151
+ // ... other config
152
+ })
153
+ ```
154
+
155
+ ### Custom Retry Policy
156
+
157
+ ```typescript
158
+ const executor = startOfflineExecutor({
159
+ maxConcurrency: 5,
160
+ jitter: true,
161
+ beforeRetry: (transactions) => {
162
+ // Filter out old transactions
163
+ const cutoff = Date.now() - 24 * 60 * 60 * 1000 // 24 hours
164
+ return transactions.filter((tx) => tx.createdAt.getTime() > cutoff)
165
+ },
166
+ // ... other config
167
+ })
168
+ ```
169
+
170
+ ### Manual Transaction Control
171
+
172
+ ```typescript
173
+ const tx = executor.createOfflineTransaction({
174
+ mutationFnName: "syncData",
175
+ autoCommit: false,
176
+ })
177
+
178
+ tx.mutate(() => {
179
+ collection.insert({ id: "1", text: "Item 1" })
180
+ collection.insert({ id: "2", text: "Item 2" })
181
+ })
182
+
183
+ // Commit when ready
184
+ await tx.commit()
185
+ ```
186
+
187
+ ## Migration from TanStack DB
188
+
189
+ This package uses explicit offline transactions to provide offline capabilities:
190
+
191
+ ```typescript
192
+ // Before: Standard TanStack DB (online only)
193
+ todoCollection.insert({ id: "1", text: "Buy milk" })
194
+
195
+ // After: Explicit offline transactions
196
+ const offline = startOfflineExecutor({
197
+ collections: { todos: todoCollection },
198
+ mutationFns: {
199
+ syncTodos: async ({ transaction }) => {
200
+ await api.sync(transaction.mutations)
201
+ },
202
+ },
203
+ })
204
+
205
+ const tx = offline.createOfflineTransaction({ mutationFnName: "syncTodos" })
206
+ tx.mutate(() => todoCollection.insert({ id: "1", text: "Buy milk" }))
207
+ await tx.commit() // Works offline!
208
+ ```
209
+
210
+ ## Browser Support
211
+
212
+ - **IndexedDB**: Modern browsers (primary storage)
213
+ - **localStorage**: Fallback for limited environments
214
+ - **Web Locks API**: Chrome 69+, Firefox 96+ (preferred leader election)
215
+ - **BroadcastChannel**: All modern browsers (fallback leader election)
216
+
217
+ ## License
218
+
219
+ MIT
@@ -0,0 +1,266 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const db = require("@tanstack/db");
4
+ const IndexedDBAdapter = require("./storage/IndexedDBAdapter.cjs");
5
+ const LocalStorageAdapter = require("./storage/LocalStorageAdapter.cjs");
6
+ const OutboxManager = require("./outbox/OutboxManager.cjs");
7
+ const KeyScheduler = require("./executor/KeyScheduler.cjs");
8
+ const TransactionExecutor = require("./executor/TransactionExecutor.cjs");
9
+ const WebLocksLeader = require("./coordination/WebLocksLeader.cjs");
10
+ const BroadcastChannelLeader = require("./coordination/BroadcastChannelLeader.cjs");
11
+ const OnlineDetector = require("./connectivity/OnlineDetector.cjs");
12
+ const OfflineTransaction = require("./api/OfflineTransaction.cjs");
13
+ const OfflineAction = require("./api/OfflineAction.cjs");
14
+ const tracer = require("./telemetry/tracer.cjs");
15
+ class OfflineExecutor {
16
+ constructor(config) {
17
+ this.isLeaderState = false;
18
+ this.unsubscribeOnline = null;
19
+ this.unsubscribeLeadership = null;
20
+ this.pendingTransactionPromises = /* @__PURE__ */ new Map();
21
+ this.config = config;
22
+ this.storage = this.createStorage();
23
+ this.outbox = new OutboxManager.OutboxManager(this.storage, this.config.collections);
24
+ this.scheduler = new KeyScheduler.KeyScheduler();
25
+ this.executor = new TransactionExecutor.TransactionExecutor(
26
+ this.scheduler,
27
+ this.outbox,
28
+ this.config,
29
+ this
30
+ );
31
+ this.leaderElection = this.createLeaderElection();
32
+ this.onlineDetector = new OnlineDetector.DefaultOnlineDetector();
33
+ this.setupEventListeners();
34
+ this.initialize();
35
+ }
36
+ createStorage() {
37
+ if (this.config.storage) {
38
+ return this.config.storage;
39
+ }
40
+ try {
41
+ return new IndexedDBAdapter.IndexedDBAdapter();
42
+ } catch (error) {
43
+ console.warn(
44
+ `IndexedDB not available, falling back to localStorage:`,
45
+ error
46
+ );
47
+ return new LocalStorageAdapter.LocalStorageAdapter();
48
+ }
49
+ }
50
+ createLeaderElection() {
51
+ if (this.config.leaderElection) {
52
+ return this.config.leaderElection;
53
+ }
54
+ if (WebLocksLeader.WebLocksLeader.isSupported()) {
55
+ return new WebLocksLeader.WebLocksLeader();
56
+ } else if (BroadcastChannelLeader.BroadcastChannelLeader.isSupported()) {
57
+ return new BroadcastChannelLeader.BroadcastChannelLeader();
58
+ } else {
59
+ return {
60
+ requestLeadership: () => Promise.resolve(true),
61
+ releaseLeadership: () => {
62
+ },
63
+ isLeader: () => true,
64
+ onLeadershipChange: () => () => {
65
+ }
66
+ };
67
+ }
68
+ }
69
+ setupEventListeners() {
70
+ this.unsubscribeLeadership = this.leaderElection.onLeadershipChange(
71
+ (isLeader) => {
72
+ this.isLeaderState = isLeader;
73
+ if (this.config.onLeadershipChange) {
74
+ this.config.onLeadershipChange(isLeader);
75
+ }
76
+ if (isLeader) {
77
+ this.loadAndReplayTransactions();
78
+ }
79
+ }
80
+ );
81
+ this.unsubscribeOnline = this.onlineDetector.subscribe(() => {
82
+ if (this.isOfflineEnabled) {
83
+ this.executor.resetRetryDelays();
84
+ this.executor.executeAll().catch((error) => {
85
+ console.warn(
86
+ `Failed to execute transactions on connectivity change:`,
87
+ error
88
+ );
89
+ });
90
+ }
91
+ });
92
+ }
93
+ async initialize() {
94
+ return tracer.withSpan(`executor.initialize`, {}, async (span) => {
95
+ try {
96
+ const isLeader = await this.leaderElection.requestLeadership();
97
+ span.setAttribute(`isLeader`, isLeader);
98
+ if (isLeader) {
99
+ await this.loadAndReplayTransactions();
100
+ }
101
+ } catch (error) {
102
+ console.warn(`Failed to initialize offline executor:`, error);
103
+ }
104
+ });
105
+ }
106
+ async loadAndReplayTransactions() {
107
+ try {
108
+ await this.executor.loadPendingTransactions();
109
+ await this.executor.executeAll();
110
+ } catch (error) {
111
+ console.warn(`Failed to load and replay transactions:`, error);
112
+ }
113
+ }
114
+ get isOfflineEnabled() {
115
+ return this.isLeaderState;
116
+ }
117
+ createOfflineTransaction(options) {
118
+ const mutationFn = this.config.mutationFns[options.mutationFnName];
119
+ if (!mutationFn) {
120
+ throw new Error(`Unknown mutation function: ${options.mutationFnName}`);
121
+ }
122
+ if (!this.isOfflineEnabled) {
123
+ return db.createTransaction({
124
+ autoCommit: options.autoCommit ?? true,
125
+ mutationFn: (params) => mutationFn({
126
+ ...params,
127
+ idempotencyKey: options.idempotencyKey || crypto.randomUUID()
128
+ }),
129
+ metadata: options.metadata
130
+ });
131
+ }
132
+ return new OfflineTransaction.OfflineTransaction(
133
+ options,
134
+ mutationFn,
135
+ this.persistTransaction.bind(this),
136
+ this
137
+ );
138
+ }
139
+ createOfflineAction(options) {
140
+ const mutationFn = this.config.mutationFns[options.mutationFnName];
141
+ if (!mutationFn) {
142
+ throw new Error(`Unknown mutation function: ${options.mutationFnName}`);
143
+ }
144
+ return (variables) => {
145
+ if (!this.isOfflineEnabled) {
146
+ const action2 = db.createOptimisticAction({
147
+ mutationFn: (vars, params) => mutationFn({
148
+ ...vars,
149
+ ...params,
150
+ idempotencyKey: crypto.randomUUID()
151
+ }),
152
+ onMutate: options.onMutate
153
+ });
154
+ return action2(variables);
155
+ }
156
+ const action = OfflineAction.createOfflineAction(
157
+ options,
158
+ mutationFn,
159
+ this.persistTransaction.bind(this),
160
+ this
161
+ );
162
+ return action(variables);
163
+ };
164
+ }
165
+ async persistTransaction(transaction) {
166
+ return tracer.withNestedSpan(
167
+ `executor.persistTransaction`,
168
+ {
169
+ "transaction.id": transaction.id,
170
+ "transaction.mutationFnName": transaction.mutationFnName
171
+ },
172
+ async (span) => {
173
+ if (!this.isOfflineEnabled) {
174
+ span.setAttribute(`result`, `skipped_not_leader`);
175
+ this.resolveTransaction(transaction.id, void 0);
176
+ return;
177
+ }
178
+ try {
179
+ await this.outbox.add(transaction);
180
+ await this.executor.execute(transaction);
181
+ span.setAttribute(`result`, `persisted`);
182
+ } catch (error) {
183
+ console.error(
184
+ `Failed to persist offline transaction ${transaction.id}:`,
185
+ error
186
+ );
187
+ span.setAttribute(`result`, `failed`);
188
+ throw error;
189
+ }
190
+ }
191
+ );
192
+ }
193
+ // Method for OfflineTransaction to wait for completion
194
+ async waitForTransactionCompletion(transactionId) {
195
+ const existing = this.pendingTransactionPromises.get(transactionId);
196
+ if (existing) {
197
+ return existing.promise;
198
+ }
199
+ const deferred = {};
200
+ deferred.promise = new Promise((resolve, reject) => {
201
+ deferred.resolve = resolve;
202
+ deferred.reject = reject;
203
+ });
204
+ this.pendingTransactionPromises.set(transactionId, deferred);
205
+ return deferred.promise;
206
+ }
207
+ // Method for TransactionExecutor to signal completion
208
+ resolveTransaction(transactionId, result) {
209
+ const deferred = this.pendingTransactionPromises.get(transactionId);
210
+ if (deferred) {
211
+ deferred.resolve(result);
212
+ this.pendingTransactionPromises.delete(transactionId);
213
+ }
214
+ }
215
+ // Method for TransactionExecutor to signal failure
216
+ rejectTransaction(transactionId, error) {
217
+ const deferred = this.pendingTransactionPromises.get(transactionId);
218
+ if (deferred) {
219
+ deferred.reject(error);
220
+ this.pendingTransactionPromises.delete(transactionId);
221
+ }
222
+ }
223
+ async removeFromOutbox(id) {
224
+ await this.outbox.remove(id);
225
+ }
226
+ async peekOutbox() {
227
+ return this.outbox.getAll();
228
+ }
229
+ async clearOutbox() {
230
+ await this.outbox.clear();
231
+ this.executor.clear();
232
+ }
233
+ notifyOnline() {
234
+ this.onlineDetector.notifyOnline();
235
+ }
236
+ getPendingCount() {
237
+ return this.executor.getPendingCount();
238
+ }
239
+ getRunningCount() {
240
+ return this.executor.getRunningCount();
241
+ }
242
+ getOnlineDetector() {
243
+ return this.onlineDetector;
244
+ }
245
+ dispose() {
246
+ if (this.unsubscribeOnline) {
247
+ this.unsubscribeOnline();
248
+ this.unsubscribeOnline = null;
249
+ }
250
+ if (this.unsubscribeLeadership) {
251
+ this.unsubscribeLeadership();
252
+ this.unsubscribeLeadership = null;
253
+ }
254
+ this.leaderElection.releaseLeadership();
255
+ this.onlineDetector.dispose();
256
+ if (`dispose` in this.leaderElection) {
257
+ this.leaderElection.dispose();
258
+ }
259
+ }
260
+ }
261
+ function startOfflineExecutor(config) {
262
+ return new OfflineExecutor(config);
263
+ }
264
+ exports.OfflineExecutor = OfflineExecutor;
265
+ exports.startOfflineExecutor = startOfflineExecutor;
266
+ //# sourceMappingURL=OfflineExecutor.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OfflineExecutor.cjs","sources":["../../src/OfflineExecutor.ts"],"sourcesContent":["// Storage adapters\nimport { createOptimisticAction, createTransaction } from \"@tanstack/db\"\nimport { IndexedDBAdapter } from \"./storage/IndexedDBAdapter\"\nimport { LocalStorageAdapter } from \"./storage/LocalStorageAdapter\"\n\n// Core components\nimport { OutboxManager } from \"./outbox/OutboxManager\"\nimport { KeyScheduler } from \"./executor/KeyScheduler\"\nimport { TransactionExecutor } from \"./executor/TransactionExecutor\"\n\n// Coordination\nimport { WebLocksLeader } from \"./coordination/WebLocksLeader\"\nimport { BroadcastChannelLeader } from \"./coordination/BroadcastChannelLeader\"\n\n// Connectivity\nimport { DefaultOnlineDetector } from \"./connectivity/OnlineDetector\"\n\n// API\nimport { OfflineTransaction as OfflineTransactionAPI } from \"./api/OfflineTransaction\"\nimport { createOfflineAction } from \"./api/OfflineAction\"\n\n// TanStack DB primitives\n\n// Replay\nimport { withNestedSpan, withSpan } from \"./telemetry/tracer\"\nimport type {\n CreateOfflineActionOptions,\n CreateOfflineTransactionOptions,\n LeaderElection,\n OfflineConfig,\n OfflineTransaction,\n StorageAdapter,\n} from \"./types\"\nimport type { Transaction } from \"@tanstack/db\"\n\nexport class OfflineExecutor {\n private config: OfflineConfig\n private storage: StorageAdapter\n private outbox: OutboxManager\n private scheduler: KeyScheduler\n private executor: TransactionExecutor\n private leaderElection: LeaderElection\n private onlineDetector: DefaultOnlineDetector\n private isLeaderState = false\n private unsubscribeOnline: (() => void) | null = null\n private unsubscribeLeadership: (() => void) | null = null\n\n // Coordination mechanism for blocking transactions\n private pendingTransactionPromises: Map<\n string,\n {\n promise: Promise<any>\n resolve: (result: any) => void\n reject: (error: Error) => void\n }\n > = new Map()\n\n constructor(config: OfflineConfig) {\n this.config = config\n this.storage = this.createStorage()\n this.outbox = new OutboxManager(this.storage, this.config.collections)\n this.scheduler = new KeyScheduler()\n this.executor = new TransactionExecutor(\n this.scheduler,\n this.outbox,\n this.config,\n this\n )\n this.leaderElection = this.createLeaderElection()\n this.onlineDetector = new DefaultOnlineDetector()\n\n this.setupEventListeners()\n this.initialize()\n }\n\n private createStorage(): StorageAdapter {\n if (this.config.storage) {\n return this.config.storage\n }\n\n try {\n return new IndexedDBAdapter()\n } catch (error) {\n console.warn(\n `IndexedDB not available, falling back to localStorage:`,\n error\n )\n return new LocalStorageAdapter()\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 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 this.unsubscribeOnline = this.onlineDetector.subscribe(() => {\n if (this.isOfflineEnabled) {\n // Reset retry delays so transactions can execute immediately when back online\n this.executor.resetRetryDelays()\n this.executor.executeAll().catch((error) => {\n console.warn(\n `Failed to execute transactions on connectivity change:`,\n error\n )\n })\n }\n })\n }\n\n private async initialize(): Promise<void> {\n return withSpan(`executor.initialize`, {}, async (span) => {\n try {\n const isLeader = await this.leaderElection.requestLeadership()\n span.setAttribute(`isLeader`, isLeader)\n\n if (isLeader) {\n await this.loadAndReplayTransactions()\n }\n } catch (error) {\n console.warn(`Failed to initialize offline executor:`, error)\n }\n })\n }\n\n private async loadAndReplayTransactions(): Promise<void> {\n try {\n await this.executor.loadPendingTransactions()\n await this.executor.executeAll()\n } catch (error) {\n console.warn(`Failed to load and replay transactions:`, error)\n }\n }\n\n get isOfflineEnabled(): boolean {\n return this.isLeaderState\n }\n\n createOfflineTransaction(\n options: CreateOfflineTransactionOptions\n ): Transaction | OfflineTransactionAPI {\n const mutationFn = this.config.mutationFns[options.mutationFnName]\n\n if (!mutationFn) {\n throw new Error(`Unknown mutation function: ${options.mutationFnName}`)\n }\n\n // Check leadership immediately and use the appropriate primitive\n if (!this.isOfflineEnabled) {\n // Non-leader: use createTransaction directly with the resolved mutation function\n // We need to wrap it to add the idempotency key\n return createTransaction({\n autoCommit: options.autoCommit ?? true,\n mutationFn: (params) =>\n mutationFn({\n ...params,\n idempotencyKey: options.idempotencyKey || crypto.randomUUID(),\n }),\n metadata: options.metadata,\n })\n }\n\n // Leader: use OfflineTransaction wrapper for offline persistence\n return new OfflineTransactionAPI(\n options,\n mutationFn,\n this.persistTransaction.bind(this),\n this\n )\n }\n\n createOfflineAction<T>(options: CreateOfflineActionOptions<T>) {\n const mutationFn = this.config.mutationFns[options.mutationFnName]\n\n if (!mutationFn) {\n throw new Error(`Unknown mutation function: ${options.mutationFnName}`)\n }\n\n // Return a wrapper that checks leadership status at call time\n return (variables: T) => {\n // Check leadership when action is called, not when it's created\n if (!this.isOfflineEnabled) {\n // Non-leader: use createOptimisticAction directly\n const action = createOptimisticAction({\n mutationFn: (vars, params) =>\n mutationFn({\n ...vars,\n ...params,\n idempotencyKey: crypto.randomUUID(),\n }),\n onMutate: options.onMutate,\n })\n return action(variables)\n }\n\n // Leader: use the offline action wrapper\n const action = createOfflineAction(\n options,\n mutationFn,\n this.persistTransaction.bind(this),\n this\n )\n return action(variables)\n }\n }\n\n private async persistTransaction(\n transaction: OfflineTransaction\n ): Promise<void> {\n return withNestedSpan(\n `executor.persistTransaction`,\n {\n \"transaction.id\": transaction.id,\n \"transaction.mutationFnName\": transaction.mutationFnName,\n },\n async (span) => {\n if (!this.isOfflineEnabled) {\n span.setAttribute(`result`, `skipped_not_leader`)\n this.resolveTransaction(transaction.id, undefined)\n return\n }\n\n try {\n await this.outbox.add(transaction)\n await this.executor.execute(transaction)\n span.setAttribute(`result`, `persisted`)\n } catch (error) {\n console.error(\n `Failed to persist offline transaction ${transaction.id}:`,\n error\n )\n span.setAttribute(`result`, `failed`)\n throw error\n }\n }\n )\n }\n\n // Method for OfflineTransaction to wait for completion\n async waitForTransactionCompletion(transactionId: string): Promise<any> {\n const existing = this.pendingTransactionPromises.get(transactionId)\n if (existing) {\n return existing.promise\n }\n\n const deferred: {\n promise: Promise<any>\n resolve: (result: any) => void\n reject: (error: Error) => void\n } = {} as any\n\n deferred.promise = new Promise((resolve, reject) => {\n deferred.resolve = resolve\n deferred.reject = reject\n })\n\n this.pendingTransactionPromises.set(transactionId, deferred)\n return deferred.promise\n }\n\n // Method for TransactionExecutor to signal completion\n resolveTransaction(transactionId: string, result: any): void {\n const deferred = this.pendingTransactionPromises.get(transactionId)\n if (deferred) {\n deferred.resolve(result)\n this.pendingTransactionPromises.delete(transactionId)\n }\n }\n\n // Method for TransactionExecutor to signal failure\n rejectTransaction(transactionId: string, error: Error): void {\n const deferred = this.pendingTransactionPromises.get(transactionId)\n if (deferred) {\n deferred.reject(error)\n this.pendingTransactionPromises.delete(transactionId)\n }\n }\n\n async removeFromOutbox(id: string): Promise<void> {\n await this.outbox.remove(id)\n }\n\n async peekOutbox(): Promise<Array<OfflineTransaction>> {\n return this.outbox.getAll()\n }\n\n async clearOutbox(): Promise<void> {\n await this.outbox.clear()\n this.executor.clear()\n }\n\n notifyOnline(): void {\n this.onlineDetector.notifyOnline()\n }\n\n getPendingCount(): number {\n return this.executor.getPendingCount()\n }\n\n getRunningCount(): number {\n return this.executor.getRunningCount()\n }\n\n getOnlineDetector(): DefaultOnlineDetector {\n return this.onlineDetector\n }\n\n dispose(): void {\n if (this.unsubscribeOnline) {\n this.unsubscribeOnline()\n this.unsubscribeOnline = null\n }\n\n if (this.unsubscribeLeadership) {\n this.unsubscribeLeadership()\n this.unsubscribeLeadership = null\n }\n\n this.leaderElection.releaseLeadership()\n this.onlineDetector.dispose()\n\n if (`dispose` in this.leaderElection) {\n ;(this.leaderElection as any).dispose()\n }\n }\n}\n\nexport function startOfflineExecutor(config: OfflineConfig): OfflineExecutor {\n return new OfflineExecutor(config)\n}\n"],"names":["OutboxManager","KeyScheduler","TransactionExecutor","DefaultOnlineDetector","IndexedDBAdapter","LocalStorageAdapter","WebLocksLeader","BroadcastChannelLeader","withSpan","createTransaction","OfflineTransactionAPI","action","createOptimisticAction","createOfflineAction","withNestedSpan"],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ import { DefaultOnlineDetector } from './connectivity/OnlineDetector.cjs';
2
+ import { OfflineTransaction as OfflineTransactionAPI } from './api/OfflineTransaction.cjs';
3
+ import { CreateOfflineActionOptions, CreateOfflineTransactionOptions, OfflineConfig, OfflineTransaction } from './types.cjs';
4
+ import { Transaction } from '@tanstack/db';
5
+ export declare class OfflineExecutor {
6
+ private config;
7
+ private storage;
8
+ private outbox;
9
+ private scheduler;
10
+ private executor;
11
+ private leaderElection;
12
+ private onlineDetector;
13
+ private isLeaderState;
14
+ private unsubscribeOnline;
15
+ private unsubscribeLeadership;
16
+ private pendingTransactionPromises;
17
+ constructor(config: OfflineConfig);
18
+ private createStorage;
19
+ private createLeaderElection;
20
+ private setupEventListeners;
21
+ private initialize;
22
+ private loadAndReplayTransactions;
23
+ get isOfflineEnabled(): boolean;
24
+ createOfflineTransaction(options: CreateOfflineTransactionOptions): Transaction | OfflineTransactionAPI;
25
+ createOfflineAction<T>(options: CreateOfflineActionOptions<T>): (variables: T) => Transaction<Record<string, unknown>>;
26
+ private persistTransaction;
27
+ waitForTransactionCompletion(transactionId: string): Promise<any>;
28
+ resolveTransaction(transactionId: string, result: any): void;
29
+ rejectTransaction(transactionId: string, error: Error): void;
30
+ removeFromOutbox(id: string): Promise<void>;
31
+ peekOutbox(): Promise<Array<OfflineTransaction>>;
32
+ clearOutbox(): Promise<void>;
33
+ notifyOnline(): void;
34
+ getPendingCount(): number;
35
+ getRunningCount(): number;
36
+ getOnlineDetector(): DefaultOnlineDetector;
37
+ dispose(): void;
38
+ }
39
+ export declare function startOfflineExecutor(config: OfflineConfig): OfflineExecutor;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const api = require("@opentelemetry/api");
4
+ const OfflineTransaction = require("./OfflineTransaction.cjs");
5
+ function createOfflineAction(options, mutationFn, persistTransaction, executor) {
6
+ const { mutationFnName, onMutate } = options;
7
+ console.log(`createOfflineAction 2`, options);
8
+ return (variables) => {
9
+ const offlineTransaction = new OfflineTransaction.OfflineTransaction(
10
+ {
11
+ mutationFnName,
12
+ autoCommit: false
13
+ },
14
+ mutationFn,
15
+ persistTransaction,
16
+ executor
17
+ );
18
+ const transaction = offlineTransaction.mutate(() => {
19
+ console.log(`mutate`);
20
+ onMutate(variables);
21
+ });
22
+ const tracer = api.trace.getTracer(`@tanstack/offline-transactions`, `0.0.1`);
23
+ const span = tracer.startSpan(`offlineAction.${mutationFnName}`);
24
+ const ctx = api.trace.setSpan(api.context.active(), span);
25
+ console.log(`starting offlineAction span`, { tracer, span, ctx });
26
+ const commitPromise = api.context.with(ctx, () => {
27
+ return (async () => {
28
+ try {
29
+ await transaction.commit();
30
+ span.setStatus({ code: api.SpanStatusCode.OK });
31
+ span.end();
32
+ console.log(`ended offlineAction span - success`);
33
+ } catch (error) {
34
+ span.recordException(error);
35
+ span.setStatus({ code: api.SpanStatusCode.ERROR });
36
+ span.end();
37
+ console.log(`ended offlineAction span - error`);
38
+ }
39
+ })();
40
+ });
41
+ commitPromise.catch(() => {
42
+ });
43
+ return transaction;
44
+ };
45
+ }
46
+ exports.createOfflineAction = createOfflineAction;
47
+ //# sourceMappingURL=OfflineAction.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OfflineAction.cjs","sources":["../../../src/api/OfflineAction.ts"],"sourcesContent":["import { SpanStatusCode, context, trace } from \"@opentelemetry/api\"\nimport { OfflineTransaction } from \"./OfflineTransaction\"\nimport type { Transaction } from \"@tanstack/db\"\nimport type {\n CreateOfflineActionOptions,\n OfflineMutationFn,\n OfflineTransaction as OfflineTransactionType,\n} from \"../types\"\n\nexport function createOfflineAction<T>(\n options: CreateOfflineActionOptions<T>,\n mutationFn: OfflineMutationFn,\n persistTransaction: (tx: OfflineTransactionType) => Promise<void>,\n executor: any\n): (variables: T) => Transaction {\n const { mutationFnName, onMutate } = options\n console.log(`createOfflineAction 2`, options)\n\n return (variables: T): Transaction => {\n const offlineTransaction = new OfflineTransaction(\n {\n mutationFnName,\n autoCommit: false,\n },\n mutationFn,\n persistTransaction,\n executor\n )\n\n const transaction = offlineTransaction.mutate(() => {\n console.log(`mutate`)\n onMutate(variables)\n })\n\n // Immediately commit with span instrumentation\n const tracer = trace.getTracer(`@tanstack/offline-transactions`, `0.0.1`)\n const span = tracer.startSpan(`offlineAction.${mutationFnName}`)\n const ctx = trace.setSpan(context.active(), span)\n console.log(`starting offlineAction span`, { tracer, span, ctx })\n\n // Execute the commit within the span context\n // The key is to return the promise synchronously from context.with() so context binds to it\n const commitPromise = context.with(ctx, () => {\n // Return the promise synchronously - this is critical for context propagation in browsers\n return (async () => {\n try {\n await transaction.commit()\n span.setStatus({ code: SpanStatusCode.OK })\n span.end()\n console.log(`ended offlineAction span - success`)\n } catch (error) {\n span.recordException(error as Error)\n span.setStatus({ code: SpanStatusCode.ERROR })\n span.end()\n console.log(`ended offlineAction span - error`)\n }\n })()\n })\n\n // Don't await - this is fire-and-forget for optimistic actions\n // But catch to prevent unhandled rejection\n commitPromise.catch(() => {\n // Already handled in try/catch above\n })\n\n return transaction\n }\n}\n"],"names":["OfflineTransaction","trace","context","SpanStatusCode"],"mappings":";;;;AASO,SAAS,oBACd,SACA,YACA,oBACA,UAC+B;AAC/B,QAAM,EAAE,gBAAgB,SAAA,IAAa;AACrC,UAAQ,IAAI,yBAAyB,OAAO;AAE5C,SAAO,CAAC,cAA8B;AACpC,UAAM,qBAAqB,IAAIA,mBAAAA;AAAAA,MAC7B;AAAA,QACE;AAAA,QACA,YAAY;AAAA,MAAA;AAAA,MAEd;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAGF,UAAM,cAAc,mBAAmB,OAAO,MAAM;AAClD,cAAQ,IAAI,QAAQ;AACpB,eAAS,SAAS;AAAA,IACpB,CAAC;AAGD,UAAM,SAASC,IAAAA,MAAM,UAAU,kCAAkC,OAAO;AACxE,UAAM,OAAO,OAAO,UAAU,iBAAiB,cAAc,EAAE;AAC/D,UAAM,MAAMA,IAAAA,MAAM,QAAQC,IAAAA,QAAQ,OAAA,GAAU,IAAI;AAChD,YAAQ,IAAI,+BAA+B,EAAE,QAAQ,MAAM,KAAK;AAIhE,UAAM,gBAAgBA,IAAAA,QAAQ,KAAK,KAAK,MAAM;AAE5C,cAAQ,YAAY;AAClB,YAAI;AACF,gBAAM,YAAY,OAAA;AAClB,eAAK,UAAU,EAAE,MAAMC,IAAAA,eAAe,IAAI;AAC1C,eAAK,IAAA;AACL,kBAAQ,IAAI,oCAAoC;AAAA,QAClD,SAAS,OAAO;AACd,eAAK,gBAAgB,KAAc;AACnC,eAAK,UAAU,EAAE,MAAMA,IAAAA,eAAe,OAAO;AAC7C,eAAK,IAAA;AACL,kBAAQ,IAAI,kCAAkC;AAAA,QAChD;AAAA,MACF,GAAA;AAAA,IACF,CAAC;AAID,kBAAc,MAAM,MAAM;AAAA,IAE1B,CAAC;AAED,WAAO;AAAA,EACT;AACF;;"}
@@ -0,0 +1,3 @@
1
+ import { Transaction } from '@tanstack/db';
2
+ import { CreateOfflineActionOptions, OfflineMutationFn, OfflineTransaction as OfflineTransactionType } from '../types.cjs';
3
+ export declare function createOfflineAction<T>(options: CreateOfflineActionOptions<T>, mutationFn: OfflineMutationFn, persistTransaction: (tx: OfflineTransactionType) => Promise<void>, executor: any): (variables: T) => Transaction;