@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
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { BaseLeaderElection } from "./LeaderElection"
|
|
2
|
+
|
|
3
|
+
interface LeaderMessage {
|
|
4
|
+
type: `heartbeat` | `election` | `leadership-claim`
|
|
5
|
+
tabId: string
|
|
6
|
+
timestamp: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class BroadcastChannelLeader extends BaseLeaderElection {
|
|
10
|
+
private channelName: string
|
|
11
|
+
private tabId: string
|
|
12
|
+
private channel: BroadcastChannel | null = null
|
|
13
|
+
private heartbeatInterval: number | null = null
|
|
14
|
+
private electionTimeout: number | null = null
|
|
15
|
+
private lastLeaderHeartbeat = 0
|
|
16
|
+
private readonly heartbeatIntervalMs = 5000
|
|
17
|
+
private readonly electionTimeoutMs = 10000
|
|
18
|
+
|
|
19
|
+
constructor(channelName = `offline-executor-leader`) {
|
|
20
|
+
super()
|
|
21
|
+
this.channelName = channelName
|
|
22
|
+
this.tabId = crypto.randomUUID()
|
|
23
|
+
this.setupChannel()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private setupChannel(): void {
|
|
27
|
+
if (!this.isBroadcastChannelSupported()) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
this.channel = new BroadcastChannel(this.channelName)
|
|
32
|
+
this.channel.addEventListener(`message`, this.handleMessage)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private handleMessage = (event: MessageEvent<LeaderMessage>): void => {
|
|
36
|
+
const { type, tabId, timestamp } = event.data
|
|
37
|
+
|
|
38
|
+
if (tabId === this.tabId) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
switch (type) {
|
|
43
|
+
case `heartbeat`:
|
|
44
|
+
if (this.isLeaderState && tabId < this.tabId) {
|
|
45
|
+
this.releaseLeadership()
|
|
46
|
+
} else if (!this.isLeaderState) {
|
|
47
|
+
this.lastLeaderHeartbeat = timestamp
|
|
48
|
+
this.cancelElection()
|
|
49
|
+
}
|
|
50
|
+
break
|
|
51
|
+
|
|
52
|
+
case `election`:
|
|
53
|
+
if (this.isLeaderState) {
|
|
54
|
+
this.sendHeartbeat()
|
|
55
|
+
} else if (tabId > this.tabId) {
|
|
56
|
+
this.startElection()
|
|
57
|
+
}
|
|
58
|
+
break
|
|
59
|
+
|
|
60
|
+
case `leadership-claim`:
|
|
61
|
+
if (this.isLeaderState && tabId < this.tabId) {
|
|
62
|
+
this.releaseLeadership()
|
|
63
|
+
}
|
|
64
|
+
break
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async requestLeadership(): Promise<boolean> {
|
|
69
|
+
if (!this.isBroadcastChannelSupported()) {
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (this.isLeaderState) {
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.startElection()
|
|
78
|
+
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
setTimeout(() => {
|
|
81
|
+
resolve(this.isLeaderState)
|
|
82
|
+
}, 1000)
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private startElection(): void {
|
|
87
|
+
if (this.electionTimeout) {
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.sendMessage({
|
|
92
|
+
type: `election`,
|
|
93
|
+
tabId: this.tabId,
|
|
94
|
+
timestamp: Date.now(),
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
this.electionTimeout = window.setTimeout(() => {
|
|
98
|
+
const timeSinceLastHeartbeat = Date.now() - this.lastLeaderHeartbeat
|
|
99
|
+
|
|
100
|
+
if (timeSinceLastHeartbeat > this.electionTimeoutMs) {
|
|
101
|
+
this.claimLeadership()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.electionTimeout = null
|
|
105
|
+
}, this.electionTimeoutMs)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private cancelElection(): void {
|
|
109
|
+
if (this.electionTimeout) {
|
|
110
|
+
clearTimeout(this.electionTimeout)
|
|
111
|
+
this.electionTimeout = null
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private claimLeadership(): void {
|
|
116
|
+
this.notifyLeadershipChange(true)
|
|
117
|
+
this.sendMessage({
|
|
118
|
+
type: `leadership-claim`,
|
|
119
|
+
tabId: this.tabId,
|
|
120
|
+
timestamp: Date.now(),
|
|
121
|
+
})
|
|
122
|
+
this.startHeartbeat()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private startHeartbeat(): void {
|
|
126
|
+
if (this.heartbeatInterval) {
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.sendHeartbeat()
|
|
131
|
+
|
|
132
|
+
this.heartbeatInterval = window.setInterval(() => {
|
|
133
|
+
this.sendHeartbeat()
|
|
134
|
+
}, this.heartbeatIntervalMs)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private stopHeartbeat(): void {
|
|
138
|
+
if (this.heartbeatInterval) {
|
|
139
|
+
clearInterval(this.heartbeatInterval)
|
|
140
|
+
this.heartbeatInterval = null
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private sendHeartbeat(): void {
|
|
145
|
+
this.sendMessage({
|
|
146
|
+
type: `heartbeat`,
|
|
147
|
+
tabId: this.tabId,
|
|
148
|
+
timestamp: Date.now(),
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private sendMessage(message: LeaderMessage): void {
|
|
153
|
+
if (this.channel) {
|
|
154
|
+
this.channel.postMessage(message)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
releaseLeadership(): void {
|
|
159
|
+
this.stopHeartbeat()
|
|
160
|
+
this.cancelElection()
|
|
161
|
+
this.notifyLeadershipChange(false)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private isBroadcastChannelSupported(): boolean {
|
|
165
|
+
return typeof BroadcastChannel !== `undefined`
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
static isSupported(): boolean {
|
|
169
|
+
return typeof BroadcastChannel !== `undefined`
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
dispose(): void {
|
|
173
|
+
this.releaseLeadership()
|
|
174
|
+
|
|
175
|
+
if (this.channel) {
|
|
176
|
+
this.channel.removeEventListener(`message`, this.handleMessage)
|
|
177
|
+
this.channel.close()
|
|
178
|
+
this.channel = null
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { LeaderElection } from "../types"
|
|
2
|
+
|
|
3
|
+
export abstract class BaseLeaderElection implements LeaderElection {
|
|
4
|
+
protected isLeaderState = false
|
|
5
|
+
protected listeners: Set<(isLeader: boolean) => void> = new Set()
|
|
6
|
+
|
|
7
|
+
abstract requestLeadership(): Promise<boolean>
|
|
8
|
+
abstract releaseLeadership(): void
|
|
9
|
+
|
|
10
|
+
isLeader(): boolean {
|
|
11
|
+
return this.isLeaderState
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
onLeadershipChange(callback: (isLeader: boolean) => void): () => void {
|
|
15
|
+
this.listeners.add(callback)
|
|
16
|
+
|
|
17
|
+
return () => {
|
|
18
|
+
this.listeners.delete(callback)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
protected notifyLeadershipChange(isLeader: boolean): void {
|
|
23
|
+
if (this.isLeaderState !== isLeader) {
|
|
24
|
+
this.isLeaderState = isLeader
|
|
25
|
+
|
|
26
|
+
for (const listener of this.listeners) {
|
|
27
|
+
try {
|
|
28
|
+
listener(isLeader)
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.warn(`Leadership change listener error:`, error)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { BaseLeaderElection } from "./LeaderElection"
|
|
2
|
+
|
|
3
|
+
export class WebLocksLeader extends BaseLeaderElection {
|
|
4
|
+
private lockName: string
|
|
5
|
+
private releaseLock: (() => void) | null = null
|
|
6
|
+
|
|
7
|
+
constructor(lockName = `offline-executor-leader`) {
|
|
8
|
+
super()
|
|
9
|
+
this.lockName = lockName
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async requestLeadership(): Promise<boolean> {
|
|
13
|
+
if (!this.isWebLocksSupported()) {
|
|
14
|
+
return false
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (this.isLeaderState) {
|
|
18
|
+
return true
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// First try to acquire the lock with ifAvailable
|
|
23
|
+
const available = await navigator.locks.request(
|
|
24
|
+
this.lockName,
|
|
25
|
+
{
|
|
26
|
+
mode: `exclusive`,
|
|
27
|
+
ifAvailable: true,
|
|
28
|
+
},
|
|
29
|
+
(lock) => {
|
|
30
|
+
return lock !== null
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if (!available) {
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Lock is available, now acquire it for real and hold it
|
|
39
|
+
navigator.locks.request(
|
|
40
|
+
this.lockName,
|
|
41
|
+
{
|
|
42
|
+
mode: `exclusive`,
|
|
43
|
+
},
|
|
44
|
+
async (lock) => {
|
|
45
|
+
if (lock) {
|
|
46
|
+
this.notifyLeadershipChange(true)
|
|
47
|
+
// Hold the lock until released
|
|
48
|
+
return new Promise<void>((resolve) => {
|
|
49
|
+
this.releaseLock = () => {
|
|
50
|
+
this.notifyLeadershipChange(false)
|
|
51
|
+
resolve()
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return true
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error instanceof Error && error.name === `AbortError`) {
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
console.warn(`Web Locks leadership request failed:`, error)
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
releaseLeadership(): void {
|
|
69
|
+
if (this.releaseLock) {
|
|
70
|
+
this.releaseLock()
|
|
71
|
+
this.releaseLock = null
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private isWebLocksSupported(): boolean {
|
|
76
|
+
return typeof navigator !== `undefined` && `locks` in navigator
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static isSupported(): boolean {
|
|
80
|
+
return typeof navigator !== `undefined` && `locks` in navigator
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { withSyncSpan } from "../telemetry/tracer"
|
|
2
|
+
import type { OfflineTransaction } from "../types"
|
|
3
|
+
|
|
4
|
+
export class KeyScheduler {
|
|
5
|
+
private pendingTransactions: Array<OfflineTransaction> = []
|
|
6
|
+
private isRunning = false
|
|
7
|
+
|
|
8
|
+
schedule(transaction: OfflineTransaction): void {
|
|
9
|
+
withSyncSpan(
|
|
10
|
+
`scheduler.schedule`,
|
|
11
|
+
{
|
|
12
|
+
"transaction.id": transaction.id,
|
|
13
|
+
queueLength: this.pendingTransactions.length,
|
|
14
|
+
},
|
|
15
|
+
() => {
|
|
16
|
+
this.pendingTransactions.push(transaction)
|
|
17
|
+
// Sort by creation time to maintain FIFO order
|
|
18
|
+
this.pendingTransactions.sort(
|
|
19
|
+
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getNextBatch(_maxConcurrency: number): Array<OfflineTransaction> {
|
|
26
|
+
return withSyncSpan(
|
|
27
|
+
`scheduler.getNextBatch`,
|
|
28
|
+
{ pendingCount: this.pendingTransactions.length },
|
|
29
|
+
(span) => {
|
|
30
|
+
// For sequential processing, we ignore maxConcurrency and only process one transaction at a time
|
|
31
|
+
if (this.isRunning || this.pendingTransactions.length === 0) {
|
|
32
|
+
span.setAttribute(`result`, `empty`)
|
|
33
|
+
return []
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Find the first transaction that's ready to run
|
|
37
|
+
const readyTransaction = this.pendingTransactions.find((tx) =>
|
|
38
|
+
this.isReadyToRun(tx)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if (readyTransaction) {
|
|
42
|
+
span.setAttribute(`result`, `found`)
|
|
43
|
+
span.setAttribute(`transaction.id`, readyTransaction.id)
|
|
44
|
+
} else {
|
|
45
|
+
span.setAttribute(`result`, `none_ready`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return readyTransaction ? [readyTransaction] : []
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private isReadyToRun(transaction: OfflineTransaction): boolean {
|
|
54
|
+
return Date.now() >= transaction.nextAttemptAt
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
markStarted(_transaction: OfflineTransaction): void {
|
|
58
|
+
this.isRunning = true
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
markCompleted(transaction: OfflineTransaction): void {
|
|
62
|
+
this.removeTransaction(transaction)
|
|
63
|
+
this.isRunning = false
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
markFailed(_transaction: OfflineTransaction): void {
|
|
67
|
+
this.isRunning = false
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private removeTransaction(transaction: OfflineTransaction): void {
|
|
71
|
+
const index = this.pendingTransactions.findIndex(
|
|
72
|
+
(tx) => tx.id === transaction.id
|
|
73
|
+
)
|
|
74
|
+
if (index >= 0) {
|
|
75
|
+
this.pendingTransactions.splice(index, 1)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
updateTransaction(transaction: OfflineTransaction): void {
|
|
80
|
+
const index = this.pendingTransactions.findIndex(
|
|
81
|
+
(tx) => tx.id === transaction.id
|
|
82
|
+
)
|
|
83
|
+
if (index >= 0) {
|
|
84
|
+
this.pendingTransactions[index] = transaction
|
|
85
|
+
// Re-sort to maintain FIFO order after update
|
|
86
|
+
this.pendingTransactions.sort(
|
|
87
|
+
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getPendingCount(): number {
|
|
93
|
+
return this.pendingTransactions.length
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getRunningCount(): number {
|
|
97
|
+
return this.isRunning ? 1 : 0
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
clear(): void {
|
|
101
|
+
this.pendingTransactions = []
|
|
102
|
+
this.isRunning = false
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
getAllPendingTransactions(): Array<OfflineTransaction> {
|
|
106
|
+
return [...this.pendingTransactions]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
updateTransactions(updatedTransactions: Array<OfflineTransaction>): void {
|
|
110
|
+
for (const updatedTx of updatedTransactions) {
|
|
111
|
+
const index = this.pendingTransactions.findIndex(
|
|
112
|
+
(tx) => tx.id === updatedTx.id
|
|
113
|
+
)
|
|
114
|
+
if (index >= 0) {
|
|
115
|
+
this.pendingTransactions[index] = updatedTx
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Re-sort to maintain FIFO order after updates
|
|
119
|
+
this.pendingTransactions.sort(
|
|
120
|
+
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
}
|