@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.
- package/README.md +219 -0
- package/dist/cjs/OfflineExecutor.cjs +266 -0
- package/dist/cjs/OfflineExecutor.cjs.map +1 -0
- package/dist/cjs/OfflineExecutor.d.cts +39 -0
- package/dist/cjs/api/OfflineAction.cjs +47 -0
- package/dist/cjs/api/OfflineAction.cjs.map +1 -0
- package/dist/cjs/api/OfflineAction.d.cts +3 -0
- package/dist/cjs/api/OfflineTransaction.cjs +96 -0
- package/dist/cjs/api/OfflineTransaction.cjs.map +1 -0
- package/dist/cjs/api/OfflineTransaction.d.cts +18 -0
- package/dist/cjs/connectivity/OnlineDetector.cjs +73 -0
- package/dist/cjs/connectivity/OnlineDetector.cjs.map +1 -0
- package/dist/cjs/connectivity/OnlineDetector.d.cts +15 -0
- package/dist/cjs/coordination/BroadcastChannelLeader.cjs +146 -0
- package/dist/cjs/coordination/BroadcastChannelLeader.cjs.map +1 -0
- package/dist/cjs/coordination/BroadcastChannelLeader.d.cts +26 -0
- package/dist/cjs/coordination/LeaderElection.cjs +31 -0
- package/dist/cjs/coordination/LeaderElection.cjs.map +1 -0
- package/dist/cjs/coordination/LeaderElection.d.cts +10 -0
- package/dist/cjs/coordination/WebLocksLeader.cjs +71 -0
- package/dist/cjs/coordination/WebLocksLeader.cjs.map +1 -0
- package/dist/cjs/coordination/WebLocksLeader.d.cts +10 -0
- package/dist/cjs/executor/KeyScheduler.cjs +106 -0
- package/dist/cjs/executor/KeyScheduler.cjs.map +1 -0
- package/dist/cjs/executor/KeyScheduler.d.cts +18 -0
- package/dist/cjs/executor/TransactionExecutor.cjs +236 -0
- package/dist/cjs/executor/TransactionExecutor.cjs.map +1 -0
- package/dist/cjs/executor/TransactionExecutor.d.cts +28 -0
- package/dist/cjs/index.cjs +34 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/index.d.cts +16 -0
- package/dist/cjs/outbox/OutboxManager.cjs +114 -0
- package/dist/cjs/outbox/OutboxManager.cjs.map +1 -0
- package/dist/cjs/outbox/OutboxManager.d.cts +18 -0
- package/dist/cjs/outbox/TransactionSerializer.cjs +135 -0
- package/dist/cjs/outbox/TransactionSerializer.cjs.map +1 -0
- package/dist/cjs/outbox/TransactionSerializer.d.cts +15 -0
- package/dist/cjs/retry/BackoffCalculator.cjs +14 -0
- package/dist/cjs/retry/BackoffCalculator.cjs.map +1 -0
- package/dist/cjs/retry/BackoffCalculator.d.cts +5 -0
- package/dist/cjs/retry/NonRetriableError.d.cts +1 -0
- package/dist/cjs/retry/RetryPolicy.cjs +33 -0
- package/dist/cjs/retry/RetryPolicy.cjs.map +1 -0
- package/dist/cjs/retry/RetryPolicy.d.cts +8 -0
- package/dist/cjs/storage/IndexedDBAdapter.cjs +104 -0
- package/dist/cjs/storage/IndexedDBAdapter.cjs.map +1 -0
- package/dist/cjs/storage/IndexedDBAdapter.d.cts +14 -0
- package/dist/cjs/storage/LocalStorageAdapter.cjs +71 -0
- package/dist/cjs/storage/LocalStorageAdapter.cjs.map +1 -0
- package/dist/cjs/storage/LocalStorageAdapter.d.cts +11 -0
- package/dist/cjs/storage/StorageAdapter.cjs +6 -0
- package/dist/cjs/storage/StorageAdapter.cjs.map +1 -0
- package/dist/cjs/storage/StorageAdapter.d.cts +9 -0
- package/dist/cjs/telemetry/tracer.cjs +91 -0
- package/dist/cjs/telemetry/tracer.cjs.map +1 -0
- package/dist/cjs/telemetry/tracer.d.cts +29 -0
- package/dist/cjs/types.cjs +10 -0
- package/dist/cjs/types.cjs.map +1 -0
- package/dist/cjs/types.d.cts +101 -0
- package/dist/esm/OfflineExecutor.d.ts +39 -0
- package/dist/esm/OfflineExecutor.js +266 -0
- package/dist/esm/OfflineExecutor.js.map +1 -0
- package/dist/esm/api/OfflineAction.d.ts +3 -0
- package/dist/esm/api/OfflineAction.js +47 -0
- package/dist/esm/api/OfflineAction.js.map +1 -0
- package/dist/esm/api/OfflineTransaction.d.ts +18 -0
- package/dist/esm/api/OfflineTransaction.js +96 -0
- package/dist/esm/api/OfflineTransaction.js.map +1 -0
- package/dist/esm/connectivity/OnlineDetector.d.ts +15 -0
- package/dist/esm/connectivity/OnlineDetector.js +73 -0
- package/dist/esm/connectivity/OnlineDetector.js.map +1 -0
- package/dist/esm/coordination/BroadcastChannelLeader.d.ts +26 -0
- package/dist/esm/coordination/BroadcastChannelLeader.js +146 -0
- package/dist/esm/coordination/BroadcastChannelLeader.js.map +1 -0
- package/dist/esm/coordination/LeaderElection.d.ts +10 -0
- package/dist/esm/coordination/LeaderElection.js +31 -0
- package/dist/esm/coordination/LeaderElection.js.map +1 -0
- package/dist/esm/coordination/WebLocksLeader.d.ts +10 -0
- package/dist/esm/coordination/WebLocksLeader.js +71 -0
- package/dist/esm/coordination/WebLocksLeader.js.map +1 -0
- package/dist/esm/executor/KeyScheduler.d.ts +18 -0
- package/dist/esm/executor/KeyScheduler.js +106 -0
- package/dist/esm/executor/KeyScheduler.js.map +1 -0
- package/dist/esm/executor/TransactionExecutor.d.ts +28 -0
- package/dist/esm/executor/TransactionExecutor.js +236 -0
- package/dist/esm/executor/TransactionExecutor.js.map +1 -0
- package/dist/esm/index.d.ts +16 -0
- package/dist/esm/index.js +34 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/outbox/OutboxManager.d.ts +18 -0
- package/dist/esm/outbox/OutboxManager.js +114 -0
- package/dist/esm/outbox/OutboxManager.js.map +1 -0
- package/dist/esm/outbox/TransactionSerializer.d.ts +15 -0
- package/dist/esm/outbox/TransactionSerializer.js +135 -0
- package/dist/esm/outbox/TransactionSerializer.js.map +1 -0
- package/dist/esm/retry/BackoffCalculator.d.ts +5 -0
- package/dist/esm/retry/BackoffCalculator.js +14 -0
- package/dist/esm/retry/BackoffCalculator.js.map +1 -0
- package/dist/esm/retry/NonRetriableError.d.ts +1 -0
- package/dist/esm/retry/RetryPolicy.d.ts +8 -0
- package/dist/esm/retry/RetryPolicy.js +33 -0
- package/dist/esm/retry/RetryPolicy.js.map +1 -0
- package/dist/esm/storage/IndexedDBAdapter.d.ts +14 -0
- package/dist/esm/storage/IndexedDBAdapter.js +104 -0
- package/dist/esm/storage/IndexedDBAdapter.js.map +1 -0
- package/dist/esm/storage/LocalStorageAdapter.d.ts +11 -0
- package/dist/esm/storage/LocalStorageAdapter.js +71 -0
- package/dist/esm/storage/LocalStorageAdapter.js.map +1 -0
- package/dist/esm/storage/StorageAdapter.d.ts +9 -0
- package/dist/esm/storage/StorageAdapter.js +6 -0
- package/dist/esm/storage/StorageAdapter.js.map +1 -0
- package/dist/esm/telemetry/tracer.d.ts +29 -0
- package/dist/esm/telemetry/tracer.js +91 -0
- package/dist/esm/telemetry/tracer.js.map +1 -0
- package/dist/esm/types.d.ts +101 -0
- package/dist/esm/types.js +10 -0
- package/dist/esm/types.js.map +1 -0
- package/package.json +66 -0
- package/src/OfflineExecutor.ts +360 -0
- package/src/api/OfflineAction.ts +68 -0
- package/src/api/OfflineTransaction.ts +134 -0
- package/src/connectivity/OnlineDetector.ts +87 -0
- package/src/coordination/BroadcastChannelLeader.ts +181 -0
- package/src/coordination/LeaderElection.ts +35 -0
- package/src/coordination/WebLocksLeader.ts +82 -0
- package/src/executor/KeyScheduler.ts +123 -0
- package/src/executor/TransactionExecutor.ts +330 -0
- package/src/index.ts +47 -0
- package/src/outbox/OutboxManager.ts +141 -0
- package/src/outbox/TransactionSerializer.ts +163 -0
- package/src/retry/BackoffCalculator.ts +13 -0
- package/src/retry/NonRetriableError.ts +1 -0
- package/src/retry/RetryPolicy.ts +41 -0
- package/src/storage/IndexedDBAdapter.ts +119 -0
- package/src/storage/LocalStorageAdapter.ts +79 -0
- package/src/storage/StorageAdapter.ts +11 -0
- package/src/telemetry/tracer.ts +156 -0
- 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;
|