@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,41 @@
|
|
|
1
|
+
import { NonRetriableError } from "../types"
|
|
2
|
+
import { BackoffCalculator } from "./BackoffCalculator"
|
|
3
|
+
import type { RetryPolicy } from "../types"
|
|
4
|
+
|
|
5
|
+
export class DefaultRetryPolicy implements RetryPolicy {
|
|
6
|
+
private backoffCalculator: BackoffCalculator
|
|
7
|
+
private maxRetries: number
|
|
8
|
+
|
|
9
|
+
constructor(maxRetries = 10, jitter = true) {
|
|
10
|
+
this.backoffCalculator = new BackoffCalculator(jitter)
|
|
11
|
+
this.maxRetries = maxRetries
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
calculateDelay(retryCount: number): number {
|
|
15
|
+
return this.backoffCalculator.calculate(retryCount)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
shouldRetry(error: Error, retryCount: number): boolean {
|
|
19
|
+
if (retryCount >= this.maxRetries) {
|
|
20
|
+
return false
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (error instanceof NonRetriableError) {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (error.name === `AbortError`) {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (error.message.includes(`401`) || error.message.includes(`403`)) {
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (error.message.includes(`422`) || error.message.includes(`400`)) {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return true
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { BaseStorageAdapter } from "./StorageAdapter"
|
|
2
|
+
|
|
3
|
+
export class IndexedDBAdapter extends BaseStorageAdapter {
|
|
4
|
+
private dbName: string
|
|
5
|
+
private storeName: string
|
|
6
|
+
private db: IDBDatabase | null = null
|
|
7
|
+
|
|
8
|
+
constructor(dbName = `offline-transactions`, storeName = `transactions`) {
|
|
9
|
+
super()
|
|
10
|
+
this.dbName = dbName
|
|
11
|
+
this.storeName = storeName
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
private async openDB(): Promise<IDBDatabase> {
|
|
15
|
+
if (this.db) {
|
|
16
|
+
return this.db
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const request = indexedDB.open(this.dbName, 1)
|
|
21
|
+
|
|
22
|
+
request.onerror = () => reject(request.error)
|
|
23
|
+
request.onsuccess = () => {
|
|
24
|
+
this.db = request.result
|
|
25
|
+
resolve(this.db)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
request.onupgradeneeded = (event) => {
|
|
29
|
+
const db = (event.target as IDBOpenDBRequest).result
|
|
30
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
31
|
+
db.createObjectStore(this.storeName)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private async getStore(
|
|
38
|
+
mode: IDBTransactionMode = `readonly`
|
|
39
|
+
): Promise<IDBObjectStore> {
|
|
40
|
+
const db = await this.openDB()
|
|
41
|
+
const transaction = db.transaction([this.storeName], mode)
|
|
42
|
+
return transaction.objectStore(this.storeName)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async get(key: string): Promise<string | null> {
|
|
46
|
+
try {
|
|
47
|
+
const store = await this.getStore(`readonly`)
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const request = store.get(key)
|
|
50
|
+
request.onerror = () => reject(request.error)
|
|
51
|
+
request.onsuccess = () => resolve(request.result ?? null)
|
|
52
|
+
})
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.warn(`IndexedDB get failed:`, error)
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async set(key: string, value: string): Promise<void> {
|
|
60
|
+
try {
|
|
61
|
+
const store = await this.getStore(`readwrite`)
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const request = store.put(value, key)
|
|
64
|
+
request.onerror = () => reject(request.error)
|
|
65
|
+
request.onsuccess = () => resolve()
|
|
66
|
+
})
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (
|
|
69
|
+
error instanceof DOMException &&
|
|
70
|
+
error.name === `QuotaExceededError`
|
|
71
|
+
) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Storage quota exceeded. Consider clearing old transactions.`
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
throw error
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async delete(key: string): Promise<void> {
|
|
81
|
+
try {
|
|
82
|
+
const store = await this.getStore(`readwrite`)
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const request = store.delete(key)
|
|
85
|
+
request.onerror = () => reject(request.error)
|
|
86
|
+
request.onsuccess = () => resolve()
|
|
87
|
+
})
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.warn(`IndexedDB delete failed:`, error)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async keys(): Promise<Array<string>> {
|
|
94
|
+
try {
|
|
95
|
+
const store = await this.getStore(`readonly`)
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
const request = store.getAllKeys()
|
|
98
|
+
request.onerror = () => reject(request.error)
|
|
99
|
+
request.onsuccess = () => resolve(request.result as Array<string>)
|
|
100
|
+
})
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.warn(`IndexedDB keys failed:`, error)
|
|
103
|
+
return []
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async clear(): Promise<void> {
|
|
108
|
+
try {
|
|
109
|
+
const store = await this.getStore(`readwrite`)
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
const request = store.clear()
|
|
112
|
+
request.onerror = () => reject(request.error)
|
|
113
|
+
request.onsuccess = () => resolve()
|
|
114
|
+
})
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.warn(`IndexedDB clear failed:`, error)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { BaseStorageAdapter } from "./StorageAdapter"
|
|
2
|
+
|
|
3
|
+
export class LocalStorageAdapter extends BaseStorageAdapter {
|
|
4
|
+
private prefix: string
|
|
5
|
+
|
|
6
|
+
constructor(prefix = `offline-tx:`) {
|
|
7
|
+
super()
|
|
8
|
+
this.prefix = prefix
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
private getKey(key: string): string {
|
|
12
|
+
return `${this.prefix}${key}`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
get(key: string): Promise<string | null> {
|
|
16
|
+
try {
|
|
17
|
+
return Promise.resolve(localStorage.getItem(this.getKey(key)))
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.warn(`localStorage get failed:`, error)
|
|
20
|
+
return Promise.resolve(null)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
set(key: string, value: string): Promise<void> {
|
|
25
|
+
try {
|
|
26
|
+
localStorage.setItem(this.getKey(key), value)
|
|
27
|
+
return Promise.resolve()
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (
|
|
30
|
+
error instanceof DOMException &&
|
|
31
|
+
error.name === `QuotaExceededError`
|
|
32
|
+
) {
|
|
33
|
+
return Promise.reject(
|
|
34
|
+
new Error(
|
|
35
|
+
`Storage quota exceeded. Consider clearing old transactions.`
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
return Promise.reject(error)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
delete(key: string): Promise<void> {
|
|
44
|
+
try {
|
|
45
|
+
localStorage.removeItem(this.getKey(key))
|
|
46
|
+
return Promise.resolve()
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.warn(`localStorage delete failed:`, error)
|
|
49
|
+
return Promise.resolve()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
keys(): Promise<Array<string>> {
|
|
54
|
+
try {
|
|
55
|
+
const keys: Array<string> = []
|
|
56
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
57
|
+
const key = localStorage.key(i)
|
|
58
|
+
if (key && key.startsWith(this.prefix)) {
|
|
59
|
+
keys.push(key.slice(this.prefix.length))
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return Promise.resolve(keys)
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.warn(`localStorage keys failed:`, error)
|
|
65
|
+
return Promise.resolve([])
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async clear(): Promise<void> {
|
|
70
|
+
try {
|
|
71
|
+
const keys = await this.keys()
|
|
72
|
+
for (const key of keys) {
|
|
73
|
+
localStorage.removeItem(this.getKey(key))
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.warn(`localStorage clear failed:`, error)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { StorageAdapter } from "../types"
|
|
2
|
+
|
|
3
|
+
export abstract class BaseStorageAdapter implements StorageAdapter {
|
|
4
|
+
abstract get(key: string): Promise<string | null>
|
|
5
|
+
abstract set(key: string, value: string): Promise<void>
|
|
6
|
+
abstract delete(key: string): Promise<void>
|
|
7
|
+
abstract keys(): Promise<Array<string>>
|
|
8
|
+
abstract clear(): Promise<void>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export { type StorageAdapter }
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {
|
|
2
|
+
trace,
|
|
3
|
+
type Span,
|
|
4
|
+
SpanStatusCode,
|
|
5
|
+
context,
|
|
6
|
+
type SpanContext,
|
|
7
|
+
} from "@opentelemetry/api"
|
|
8
|
+
|
|
9
|
+
const TRACER = trace.getTracer("@tanstack/offline-transactions", "0.0.1")
|
|
10
|
+
|
|
11
|
+
export interface SpanAttrs {
|
|
12
|
+
[key: string]: string | number | boolean | undefined
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface WithSpanOptions {
|
|
16
|
+
parentContext?: SpanContext
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getParentContext(options?: WithSpanOptions) {
|
|
20
|
+
if (options?.parentContext) {
|
|
21
|
+
const parentSpan = trace.wrapSpanContext(options.parentContext)
|
|
22
|
+
return trace.setSpan(context.active(), parentSpan)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return context.active()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Lightweight span wrapper with error handling.
|
|
30
|
+
* Uses OpenTelemetry API which is no-op when tracing is disabled.
|
|
31
|
+
*
|
|
32
|
+
* By default, creates spans at the current context level (siblings).
|
|
33
|
+
* Use withNestedSpan if you want parent-child relationships.
|
|
34
|
+
*/
|
|
35
|
+
export async function withSpan<T>(
|
|
36
|
+
name: string,
|
|
37
|
+
attrs: SpanAttrs,
|
|
38
|
+
fn: (span: Span) => Promise<T>,
|
|
39
|
+
options?: WithSpanOptions
|
|
40
|
+
): Promise<T> {
|
|
41
|
+
const parentCtx = getParentContext(options)
|
|
42
|
+
const span = TRACER.startSpan(name, undefined, parentCtx)
|
|
43
|
+
|
|
44
|
+
// Filter out undefined attributes
|
|
45
|
+
const filteredAttrs: Record<string, string | number | boolean> = {}
|
|
46
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
47
|
+
if (value !== undefined) {
|
|
48
|
+
filteredAttrs[key] = value
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
span.setAttributes(filteredAttrs)
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const result = await fn(span)
|
|
56
|
+
span.setStatus({ code: SpanStatusCode.OK })
|
|
57
|
+
return result
|
|
58
|
+
} catch (error) {
|
|
59
|
+
span.setStatus({
|
|
60
|
+
code: SpanStatusCode.ERROR,
|
|
61
|
+
message: error instanceof Error ? error.message : String(error),
|
|
62
|
+
})
|
|
63
|
+
span.recordException(error as Error)
|
|
64
|
+
throw error
|
|
65
|
+
} finally {
|
|
66
|
+
span.end()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Like withSpan but propagates context so child spans nest properly.
|
|
72
|
+
* Use this when you want operations inside fn to be child spans.
|
|
73
|
+
*/
|
|
74
|
+
export async function withNestedSpan<T>(
|
|
75
|
+
name: string,
|
|
76
|
+
attrs: SpanAttrs,
|
|
77
|
+
fn: (span: Span) => Promise<T>,
|
|
78
|
+
options?: WithSpanOptions
|
|
79
|
+
): Promise<T> {
|
|
80
|
+
const parentCtx = getParentContext(options)
|
|
81
|
+
const span = TRACER.startSpan(name, undefined, parentCtx)
|
|
82
|
+
|
|
83
|
+
// Filter out undefined attributes
|
|
84
|
+
const filteredAttrs: Record<string, string | number | boolean> = {}
|
|
85
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
86
|
+
if (value !== undefined) {
|
|
87
|
+
filteredAttrs[key] = value
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
span.setAttributes(filteredAttrs)
|
|
92
|
+
|
|
93
|
+
// Set the span as active context so child spans nest properly
|
|
94
|
+
const ctx = trace.setSpan(parentCtx, span)
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// Execute the function within the span's context
|
|
98
|
+
const result = await context.with(ctx, () => fn(span))
|
|
99
|
+
span.setStatus({ code: SpanStatusCode.OK })
|
|
100
|
+
return result
|
|
101
|
+
} catch (error) {
|
|
102
|
+
span.setStatus({
|
|
103
|
+
code: SpanStatusCode.ERROR,
|
|
104
|
+
message: error instanceof Error ? error.message : String(error),
|
|
105
|
+
})
|
|
106
|
+
span.recordException(error as Error)
|
|
107
|
+
throw error
|
|
108
|
+
} finally {
|
|
109
|
+
span.end()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Creates a synchronous span for non-async operations
|
|
115
|
+
*/
|
|
116
|
+
export function withSyncSpan<T>(
|
|
117
|
+
name: string,
|
|
118
|
+
attrs: SpanAttrs,
|
|
119
|
+
fn: (span: Span) => T,
|
|
120
|
+
options?: WithSpanOptions
|
|
121
|
+
): T {
|
|
122
|
+
const parentCtx = getParentContext(options)
|
|
123
|
+
const span = TRACER.startSpan(name, undefined, parentCtx)
|
|
124
|
+
|
|
125
|
+
// Filter out undefined attributes
|
|
126
|
+
const filteredAttrs: Record<string, string | number | boolean> = {}
|
|
127
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
128
|
+
if (value !== undefined) {
|
|
129
|
+
filteredAttrs[key] = value
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
span.setAttributes(filteredAttrs)
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const result = fn(span)
|
|
137
|
+
span.setStatus({ code: SpanStatusCode.OK })
|
|
138
|
+
return result
|
|
139
|
+
} catch (error) {
|
|
140
|
+
span.setStatus({
|
|
141
|
+
code: SpanStatusCode.ERROR,
|
|
142
|
+
message: error instanceof Error ? error.message : String(error),
|
|
143
|
+
})
|
|
144
|
+
span.recordException(error as Error)
|
|
145
|
+
throw error
|
|
146
|
+
} finally {
|
|
147
|
+
span.end()
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get the current tracer instance
|
|
153
|
+
*/
|
|
154
|
+
export function getTracer() {
|
|
155
|
+
return TRACER
|
|
156
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Collection,
|
|
3
|
+
MutationFnParams,
|
|
4
|
+
PendingMutation,
|
|
5
|
+
} from "@tanstack/db"
|
|
6
|
+
|
|
7
|
+
// Extended mutation function that includes idempotency key
|
|
8
|
+
export type OfflineMutationFnParams<
|
|
9
|
+
T extends object = Record<string, unknown>,
|
|
10
|
+
> = MutationFnParams<T> & {
|
|
11
|
+
idempotencyKey: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type OfflineMutationFn<T extends object = Record<string, unknown>> = (
|
|
15
|
+
params: OfflineMutationFnParams<T>
|
|
16
|
+
) => Promise<any>
|
|
17
|
+
|
|
18
|
+
// Simplified mutation structure for serialization
|
|
19
|
+
export interface SerializedMutation {
|
|
20
|
+
globalKey: string
|
|
21
|
+
type: string
|
|
22
|
+
modified: any
|
|
23
|
+
original: any
|
|
24
|
+
collectionId: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SerializedError {
|
|
28
|
+
name: string
|
|
29
|
+
message: string
|
|
30
|
+
stack?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SerializedSpanContext {
|
|
34
|
+
traceId: string
|
|
35
|
+
spanId: string
|
|
36
|
+
traceFlags: number
|
|
37
|
+
traceState?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// In-memory representation with full PendingMutation objects
|
|
41
|
+
export interface OfflineTransaction {
|
|
42
|
+
id: string
|
|
43
|
+
mutationFnName: string
|
|
44
|
+
mutations: Array<PendingMutation>
|
|
45
|
+
keys: Array<string>
|
|
46
|
+
idempotencyKey: string
|
|
47
|
+
createdAt: Date
|
|
48
|
+
retryCount: number
|
|
49
|
+
nextAttemptAt: number
|
|
50
|
+
lastError?: SerializedError
|
|
51
|
+
metadata?: Record<string, any>
|
|
52
|
+
spanContext?: SerializedSpanContext
|
|
53
|
+
version: 1
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Serialized representation for storage
|
|
57
|
+
export interface SerializedOfflineTransaction {
|
|
58
|
+
id: string
|
|
59
|
+
mutationFnName: string
|
|
60
|
+
mutations: Array<SerializedMutation>
|
|
61
|
+
keys: Array<string>
|
|
62
|
+
idempotencyKey: string
|
|
63
|
+
createdAt: Date
|
|
64
|
+
retryCount: number
|
|
65
|
+
nextAttemptAt: number
|
|
66
|
+
lastError?: SerializedError
|
|
67
|
+
metadata?: Record<string, any>
|
|
68
|
+
spanContext?: SerializedSpanContext
|
|
69
|
+
version: 1
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface OfflineConfig {
|
|
73
|
+
collections: Record<string, Collection>
|
|
74
|
+
mutationFns: Record<string, OfflineMutationFn>
|
|
75
|
+
storage?: StorageAdapter
|
|
76
|
+
maxConcurrency?: number
|
|
77
|
+
jitter?: boolean
|
|
78
|
+
beforeRetry?: (
|
|
79
|
+
transactions: Array<OfflineTransaction>
|
|
80
|
+
) => Array<OfflineTransaction>
|
|
81
|
+
onUnknownMutationFn?: (name: string, tx: OfflineTransaction) => void
|
|
82
|
+
onLeadershipChange?: (isLeader: boolean) => void
|
|
83
|
+
leaderElection?: LeaderElection
|
|
84
|
+
otel?: {
|
|
85
|
+
endpoint: string
|
|
86
|
+
headers?: Record<string, string>
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface StorageAdapter {
|
|
91
|
+
get: (key: string) => Promise<string | null>
|
|
92
|
+
set: (key: string, value: string) => Promise<void>
|
|
93
|
+
delete: (key: string) => Promise<void>
|
|
94
|
+
keys: () => Promise<Array<string>>
|
|
95
|
+
clear: () => Promise<void>
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface RetryPolicy {
|
|
99
|
+
calculateDelay: (retryCount: number) => number
|
|
100
|
+
shouldRetry: (error: Error, retryCount: number) => boolean
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface LeaderElection {
|
|
104
|
+
requestLeadership: () => Promise<boolean>
|
|
105
|
+
releaseLeadership: () => void
|
|
106
|
+
isLeader: () => boolean
|
|
107
|
+
onLeadershipChange: (callback: (isLeader: boolean) => void) => () => void
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface OnlineDetector {
|
|
111
|
+
subscribe: (callback: () => void) => () => void
|
|
112
|
+
notifyOnline: () => void
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface CreateOfflineTransactionOptions {
|
|
116
|
+
id?: string
|
|
117
|
+
mutationFnName: string
|
|
118
|
+
autoCommit?: boolean
|
|
119
|
+
idempotencyKey?: string
|
|
120
|
+
metadata?: Record<string, any>
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface CreateOfflineActionOptions<T> {
|
|
124
|
+
mutationFnName: string
|
|
125
|
+
onMutate: (variables: T) => void
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export class NonRetriableError extends Error {
|
|
129
|
+
constructor(message: string) {
|
|
130
|
+
super(message)
|
|
131
|
+
this.name = `NonRetriableError`
|
|
132
|
+
}
|
|
133
|
+
}
|