@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
@@ -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
+ }