@tanstack/offline-transactions 0.1.2 → 0.1.3
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/dist/cjs/api/OfflineAction.cjs +8 -20
- package/dist/cjs/api/OfflineAction.cjs.map +1 -1
- package/dist/cjs/api/OfflineTransaction.cjs +1 -9
- package/dist/cjs/api/OfflineTransaction.cjs.map +1 -1
- package/dist/cjs/executor/TransactionExecutor.cjs +0 -15
- package/dist/cjs/executor/TransactionExecutor.cjs.map +1 -1
- package/dist/cjs/outbox/OutboxManager.cjs +3 -3
- package/dist/cjs/outbox/OutboxManager.cjs.map +1 -1
- package/dist/cjs/telemetry/tracer.cjs +17 -81
- package/dist/cjs/telemetry/tracer.cjs.map +1 -1
- package/dist/cjs/telemetry/tracer.d.cts +9 -8
- package/dist/esm/api/OfflineAction.js +8 -20
- package/dist/esm/api/OfflineAction.js.map +1 -1
- package/dist/esm/api/OfflineTransaction.js +1 -9
- package/dist/esm/api/OfflineTransaction.js.map +1 -1
- package/dist/esm/executor/TransactionExecutor.js +0 -15
- package/dist/esm/executor/TransactionExecutor.js.map +1 -1
- package/dist/esm/outbox/OutboxManager.js +3 -3
- package/dist/esm/outbox/OutboxManager.js.map +1 -1
- package/dist/esm/telemetry/tracer.d.ts +9 -8
- package/dist/esm/telemetry/tracer.js +17 -81
- package/dist/esm/telemetry/tracer.js.map +1 -1
- package/package.json +4 -6
- package/src/api/OfflineAction.ts +9 -25
- package/src/api/OfflineTransaction.ts +1 -12
- package/src/executor/TransactionExecutor.ts +1 -27
- package/src/telemetry/tracer.ts +22 -107
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TransactionExecutor.js","sources":["../../../src/executor/TransactionExecutor.ts"],"sourcesContent":["import { createTraceState } from \"@opentelemetry/api\"\nimport { DefaultRetryPolicy } from \"../retry/RetryPolicy\"\nimport { NonRetriableError } from \"../types\"\nimport { withNestedSpan } from \"../telemetry/tracer\"\nimport type { SpanContext } from \"@opentelemetry/api\"\nimport type { KeyScheduler } from \"./KeyScheduler\"\nimport type { OutboxManager } from \"../outbox/OutboxManager\"\nimport type {\n OfflineConfig,\n OfflineTransaction,\n SerializedSpanContext,\n} from \"../types\"\n\nconst HANDLED_EXECUTION_ERROR = Symbol(`HandledExecutionError`)\n\nfunction toSpanContext(\n serialized?: SerializedSpanContext\n): SpanContext | undefined {\n if (!serialized) {\n return undefined\n }\n\n return {\n traceId: serialized.traceId,\n spanId: serialized.spanId,\n traceFlags: serialized.traceFlags,\n traceState: serialized.traceState\n ? createTraceState(serialized.traceState)\n : undefined,\n }\n}\n\nexport class TransactionExecutor {\n private scheduler: KeyScheduler\n private outbox: OutboxManager\n private config: OfflineConfig\n private retryPolicy: DefaultRetryPolicy\n private isExecuting = false\n private executionPromise: Promise<void> | null = null\n private offlineExecutor: any // Reference to OfflineExecutor for signaling\n private retryTimer: ReturnType<typeof setTimeout> | null = null\n\n constructor(\n scheduler: KeyScheduler,\n outbox: OutboxManager,\n config: OfflineConfig,\n offlineExecutor: any\n ) {\n this.scheduler = scheduler\n this.outbox = outbox\n this.config = config\n this.retryPolicy = new DefaultRetryPolicy(10, config.jitter ?? true)\n this.offlineExecutor = offlineExecutor\n }\n\n async execute(transaction: OfflineTransaction): Promise<void> {\n this.scheduler.schedule(transaction)\n await this.executeAll()\n }\n\n async executeAll(): Promise<void> {\n if (this.isExecuting) {\n return this.executionPromise!\n }\n\n this.isExecuting = true\n this.executionPromise = this.runExecution()\n\n try {\n await this.executionPromise\n } finally {\n this.isExecuting = false\n this.executionPromise = null\n }\n }\n\n private async runExecution(): Promise<void> {\n const maxConcurrency = this.config.maxConcurrency ?? 3\n\n while (this.scheduler.getPendingCount() > 0) {\n const batch = this.scheduler.getNextBatch(maxConcurrency)\n\n if (batch.length === 0) {\n break\n }\n\n const executions = batch.map((transaction) =>\n this.executeTransaction(transaction)\n )\n await Promise.allSettled(executions)\n }\n\n // Schedule next retry after execution completes\n this.scheduleNextRetry()\n }\n\n private async executeTransaction(\n transaction: OfflineTransaction\n ): Promise<void> {\n try {\n await withNestedSpan(\n `transaction.execute`,\n {\n \"transaction.id\": transaction.id,\n \"transaction.mutationFnName\": transaction.mutationFnName,\n \"transaction.retryCount\": transaction.retryCount,\n \"transaction.keyCount\": transaction.keys.length,\n },\n async (span) => {\n this.scheduler.markStarted(transaction)\n\n if (transaction.retryCount > 0) {\n span.setAttribute(`retry.attempt`, transaction.retryCount)\n }\n\n try {\n const result = await this.runMutationFn(transaction)\n\n this.scheduler.markCompleted(transaction)\n await this.outbox.remove(transaction.id)\n\n span.setAttribute(`result`, `success`)\n this.offlineExecutor.resolveTransaction(transaction.id, result)\n } catch (error) {\n const err =\n error instanceof Error ? error : new Error(String(error))\n\n span.setAttribute(`result`, `error`)\n\n await this.handleError(transaction, err)\n ;(err as any)[HANDLED_EXECUTION_ERROR] = true\n throw err\n }\n },\n {\n parentContext: toSpanContext(transaction.spanContext),\n }\n )\n } catch (error) {\n if (\n error instanceof Error &&\n (error as any)[HANDLED_EXECUTION_ERROR] === true\n ) {\n return\n }\n\n throw error\n }\n }\n\n private async runMutationFn(transaction: OfflineTransaction): Promise<void> {\n const mutationFn = this.config.mutationFns[transaction.mutationFnName]\n\n if (!mutationFn) {\n const errorMessage = `Unknown mutation function: ${transaction.mutationFnName}`\n\n if (this.config.onUnknownMutationFn) {\n this.config.onUnknownMutationFn(transaction.mutationFnName, transaction)\n }\n\n throw new NonRetriableError(errorMessage)\n }\n\n // Mutations are already PendingMutation objects with collections attached\n // from the deserializer, so we can use them directly\n const transactionWithMutations = {\n id: transaction.id,\n mutations: transaction.mutations,\n metadata: transaction.metadata ?? {},\n }\n\n await mutationFn({\n transaction: transactionWithMutations as any,\n idempotencyKey: transaction.idempotencyKey,\n })\n }\n\n private async handleError(\n transaction: OfflineTransaction,\n error: Error\n ): Promise<void> {\n return withNestedSpan(\n `transaction.handleError`,\n {\n \"transaction.id\": transaction.id,\n \"error.name\": error.name,\n \"error.message\": error.message,\n },\n async (span) => {\n const shouldRetry = this.retryPolicy.shouldRetry(\n error,\n transaction.retryCount\n )\n\n span.setAttribute(`shouldRetry`, shouldRetry)\n\n if (!shouldRetry) {\n this.scheduler.markCompleted(transaction)\n await this.outbox.remove(transaction.id)\n console.warn(\n `Transaction ${transaction.id} failed permanently:`,\n error\n )\n\n span.setAttribute(`result`, `permanent_failure`)\n // Signal permanent failure to the waiting transaction\n this.offlineExecutor.rejectTransaction(transaction.id, error)\n return\n }\n\n const delay = this.retryPolicy.calculateDelay(transaction.retryCount)\n const updatedTransaction: OfflineTransaction = {\n ...transaction,\n retryCount: transaction.retryCount + 1,\n nextAttemptAt: Date.now() + delay,\n lastError: {\n name: error.name,\n message: error.message,\n stack: error.stack,\n },\n }\n\n span.setAttribute(`retryDelay`, delay)\n span.setAttribute(`nextRetryCount`, updatedTransaction.retryCount)\n\n this.scheduler.markFailed(transaction)\n this.scheduler.updateTransaction(updatedTransaction)\n\n try {\n await this.outbox.update(transaction.id, updatedTransaction)\n span.setAttribute(`result`, `scheduled_retry`)\n } catch (persistError) {\n span.recordException(persistError as Error)\n span.setAttribute(`result`, `persist_failed`)\n throw persistError\n }\n\n // Schedule retry timer\n this.scheduleNextRetry()\n }\n )\n }\n\n async loadPendingTransactions(): Promise<void> {\n const transactions = await this.outbox.getAll()\n let filteredTransactions = transactions\n\n if (this.config.beforeRetry) {\n filteredTransactions = this.config.beforeRetry(transactions)\n }\n\n for (const transaction of filteredTransactions) {\n this.scheduler.schedule(transaction)\n }\n\n // Reset retry delays for all loaded transactions so they can run immediately\n this.resetRetryDelays()\n\n // Schedule retry timer for loaded transactions\n this.scheduleNextRetry()\n\n const removedTransactions = transactions.filter(\n (tx) => !filteredTransactions.some((filtered) => filtered.id === tx.id)\n )\n\n if (removedTransactions.length > 0) {\n await this.outbox.removeMany(removedTransactions.map((tx) => tx.id))\n }\n }\n\n clear(): void {\n this.scheduler.clear()\n this.clearRetryTimer()\n }\n\n getPendingCount(): number {\n return this.scheduler.getPendingCount()\n }\n\n private scheduleNextRetry(): void {\n // Clear existing timer\n this.clearRetryTimer()\n\n // Find the earliest retry time among pending transactions\n const earliestRetryTime = this.getEarliestRetryTime()\n\n if (earliestRetryTime === null) {\n return // No transactions pending retry\n }\n\n const delay = Math.max(0, earliestRetryTime - Date.now())\n\n this.retryTimer = setTimeout(() => {\n this.executeAll().catch((error) => {\n console.warn(`Failed to execute retry batch:`, error)\n })\n }, delay)\n }\n\n private getEarliestRetryTime(): number | null {\n const allTransactions = this.scheduler.getAllPendingTransactions()\n\n if (allTransactions.length === 0) {\n return null\n }\n\n return Math.min(...allTransactions.map((tx) => tx.nextAttemptAt))\n }\n\n private clearRetryTimer(): void {\n if (this.retryTimer) {\n clearTimeout(this.retryTimer)\n this.retryTimer = null\n }\n }\n\n getRunningCount(): number {\n return this.scheduler.getRunningCount()\n }\n\n resetRetryDelays(): void {\n const allTransactions = this.scheduler.getAllPendingTransactions()\n const updatedTransactions = allTransactions.map((transaction) => ({\n ...transaction,\n nextAttemptAt: Date.now(),\n }))\n\n this.scheduler.updateTransactions(updatedTransactions)\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TransactionExecutor.js","sources":["../../../src/executor/TransactionExecutor.ts"],"sourcesContent":["import { DefaultRetryPolicy } from \"../retry/RetryPolicy\"\nimport { NonRetriableError } from \"../types\"\nimport { withNestedSpan } from \"../telemetry/tracer\"\nimport type { KeyScheduler } from \"./KeyScheduler\"\nimport type { OutboxManager } from \"../outbox/OutboxManager\"\nimport type { OfflineConfig, OfflineTransaction } from \"../types\"\n\nconst HANDLED_EXECUTION_ERROR = Symbol(`HandledExecutionError`)\n\nexport class TransactionExecutor {\n private scheduler: KeyScheduler\n private outbox: OutboxManager\n private config: OfflineConfig\n private retryPolicy: DefaultRetryPolicy\n private isExecuting = false\n private executionPromise: Promise<void> | null = null\n private offlineExecutor: any // Reference to OfflineExecutor for signaling\n private retryTimer: ReturnType<typeof setTimeout> | null = null\n\n constructor(\n scheduler: KeyScheduler,\n outbox: OutboxManager,\n config: OfflineConfig,\n offlineExecutor: any\n ) {\n this.scheduler = scheduler\n this.outbox = outbox\n this.config = config\n this.retryPolicy = new DefaultRetryPolicy(10, config.jitter ?? true)\n this.offlineExecutor = offlineExecutor\n }\n\n async execute(transaction: OfflineTransaction): Promise<void> {\n this.scheduler.schedule(transaction)\n await this.executeAll()\n }\n\n async executeAll(): Promise<void> {\n if (this.isExecuting) {\n return this.executionPromise!\n }\n\n this.isExecuting = true\n this.executionPromise = this.runExecution()\n\n try {\n await this.executionPromise\n } finally {\n this.isExecuting = false\n this.executionPromise = null\n }\n }\n\n private async runExecution(): Promise<void> {\n const maxConcurrency = this.config.maxConcurrency ?? 3\n\n while (this.scheduler.getPendingCount() > 0) {\n const batch = this.scheduler.getNextBatch(maxConcurrency)\n\n if (batch.length === 0) {\n break\n }\n\n const executions = batch.map((transaction) =>\n this.executeTransaction(transaction)\n )\n await Promise.allSettled(executions)\n }\n\n // Schedule next retry after execution completes\n this.scheduleNextRetry()\n }\n\n private async executeTransaction(\n transaction: OfflineTransaction\n ): Promise<void> {\n try {\n await withNestedSpan(\n `transaction.execute`,\n {\n \"transaction.id\": transaction.id,\n \"transaction.mutationFnName\": transaction.mutationFnName,\n \"transaction.retryCount\": transaction.retryCount,\n \"transaction.keyCount\": transaction.keys.length,\n },\n async (span) => {\n this.scheduler.markStarted(transaction)\n\n if (transaction.retryCount > 0) {\n span.setAttribute(`retry.attempt`, transaction.retryCount)\n }\n\n try {\n const result = await this.runMutationFn(transaction)\n\n this.scheduler.markCompleted(transaction)\n await this.outbox.remove(transaction.id)\n\n span.setAttribute(`result`, `success`)\n this.offlineExecutor.resolveTransaction(transaction.id, result)\n } catch (error) {\n const err =\n error instanceof Error ? error : new Error(String(error))\n\n span.setAttribute(`result`, `error`)\n\n await this.handleError(transaction, err)\n ;(err as any)[HANDLED_EXECUTION_ERROR] = true\n throw err\n }\n }\n )\n } catch (error) {\n if (\n error instanceof Error &&\n (error as any)[HANDLED_EXECUTION_ERROR] === true\n ) {\n return\n }\n\n throw error\n }\n }\n\n private async runMutationFn(transaction: OfflineTransaction): Promise<void> {\n const mutationFn = this.config.mutationFns[transaction.mutationFnName]\n\n if (!mutationFn) {\n const errorMessage = `Unknown mutation function: ${transaction.mutationFnName}`\n\n if (this.config.onUnknownMutationFn) {\n this.config.onUnknownMutationFn(transaction.mutationFnName, transaction)\n }\n\n throw new NonRetriableError(errorMessage)\n }\n\n // Mutations are already PendingMutation objects with collections attached\n // from the deserializer, so we can use them directly\n const transactionWithMutations = {\n id: transaction.id,\n mutations: transaction.mutations,\n metadata: transaction.metadata ?? {},\n }\n\n await mutationFn({\n transaction: transactionWithMutations as any,\n idempotencyKey: transaction.idempotencyKey,\n })\n }\n\n private async handleError(\n transaction: OfflineTransaction,\n error: Error\n ): Promise<void> {\n return withNestedSpan(\n `transaction.handleError`,\n {\n \"transaction.id\": transaction.id,\n \"error.name\": error.name,\n \"error.message\": error.message,\n },\n async (span) => {\n const shouldRetry = this.retryPolicy.shouldRetry(\n error,\n transaction.retryCount\n )\n\n span.setAttribute(`shouldRetry`, shouldRetry)\n\n if (!shouldRetry) {\n this.scheduler.markCompleted(transaction)\n await this.outbox.remove(transaction.id)\n console.warn(\n `Transaction ${transaction.id} failed permanently:`,\n error\n )\n\n span.setAttribute(`result`, `permanent_failure`)\n // Signal permanent failure to the waiting transaction\n this.offlineExecutor.rejectTransaction(transaction.id, error)\n return\n }\n\n const delay = this.retryPolicy.calculateDelay(transaction.retryCount)\n const updatedTransaction: OfflineTransaction = {\n ...transaction,\n retryCount: transaction.retryCount + 1,\n nextAttemptAt: Date.now() + delay,\n lastError: {\n name: error.name,\n message: error.message,\n stack: error.stack,\n },\n }\n\n span.setAttribute(`retryDelay`, delay)\n span.setAttribute(`nextRetryCount`, updatedTransaction.retryCount)\n\n this.scheduler.markFailed(transaction)\n this.scheduler.updateTransaction(updatedTransaction)\n\n try {\n await this.outbox.update(transaction.id, updatedTransaction)\n span.setAttribute(`result`, `scheduled_retry`)\n } catch (persistError) {\n span.recordException(persistError as Error)\n span.setAttribute(`result`, `persist_failed`)\n throw persistError\n }\n\n // Schedule retry timer\n this.scheduleNextRetry()\n }\n )\n }\n\n async loadPendingTransactions(): Promise<void> {\n const transactions = await this.outbox.getAll()\n let filteredTransactions = transactions\n\n if (this.config.beforeRetry) {\n filteredTransactions = this.config.beforeRetry(transactions)\n }\n\n for (const transaction of filteredTransactions) {\n this.scheduler.schedule(transaction)\n }\n\n // Reset retry delays for all loaded transactions so they can run immediately\n this.resetRetryDelays()\n\n // Schedule retry timer for loaded transactions\n this.scheduleNextRetry()\n\n const removedTransactions = transactions.filter(\n (tx) => !filteredTransactions.some((filtered) => filtered.id === tx.id)\n )\n\n if (removedTransactions.length > 0) {\n await this.outbox.removeMany(removedTransactions.map((tx) => tx.id))\n }\n }\n\n clear(): void {\n this.scheduler.clear()\n this.clearRetryTimer()\n }\n\n getPendingCount(): number {\n return this.scheduler.getPendingCount()\n }\n\n private scheduleNextRetry(): void {\n // Clear existing timer\n this.clearRetryTimer()\n\n // Find the earliest retry time among pending transactions\n const earliestRetryTime = this.getEarliestRetryTime()\n\n if (earliestRetryTime === null) {\n return // No transactions pending retry\n }\n\n const delay = Math.max(0, earliestRetryTime - Date.now())\n\n this.retryTimer = setTimeout(() => {\n this.executeAll().catch((error) => {\n console.warn(`Failed to execute retry batch:`, error)\n })\n }, delay)\n }\n\n private getEarliestRetryTime(): number | null {\n const allTransactions = this.scheduler.getAllPendingTransactions()\n\n if (allTransactions.length === 0) {\n return null\n }\n\n return Math.min(...allTransactions.map((tx) => tx.nextAttemptAt))\n }\n\n private clearRetryTimer(): void {\n if (this.retryTimer) {\n clearTimeout(this.retryTimer)\n this.retryTimer = null\n }\n }\n\n getRunningCount(): number {\n return this.scheduler.getRunningCount()\n }\n\n resetRetryDelays(): void {\n const allTransactions = this.scheduler.getAllPendingTransactions()\n const updatedTransactions = allTransactions.map((transaction) => ({\n ...transaction,\n nextAttemptAt: Date.now(),\n }))\n\n this.scheduler.updateTransactions(updatedTransactions)\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -25,7 +25,7 @@ class OutboxManager {
|
|
|
25
25
|
);
|
|
26
26
|
}
|
|
27
27
|
async get(id) {
|
|
28
|
-
return withSpan(`outbox.get`, {
|
|
28
|
+
return withSpan(`outbox.get`, {}, async (span) => {
|
|
29
29
|
const key = this.getStorageKey(id);
|
|
30
30
|
const data = await this.storage.get(key);
|
|
31
31
|
if (!data) {
|
|
@@ -78,7 +78,7 @@ class OutboxManager {
|
|
|
78
78
|
);
|
|
79
79
|
}
|
|
80
80
|
async update(id, updates) {
|
|
81
|
-
return withSpan(`outbox.update`, {
|
|
81
|
+
return withSpan(`outbox.update`, {}, async () => {
|
|
82
82
|
const existing = await this.get(id);
|
|
83
83
|
if (!existing) {
|
|
84
84
|
throw new Error(`Transaction ${id} not found`);
|
|
@@ -88,7 +88,7 @@ class OutboxManager {
|
|
|
88
88
|
});
|
|
89
89
|
}
|
|
90
90
|
async remove(id) {
|
|
91
|
-
return withSpan(`outbox.remove`, {
|
|
91
|
+
return withSpan(`outbox.remove`, {}, async () => {
|
|
92
92
|
const key = this.getStorageKey(id);
|
|
93
93
|
await this.storage.delete(key);
|
|
94
94
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OutboxManager.js","sources":["../../../src/outbox/OutboxManager.ts"],"sourcesContent":["import { withSpan } from \"../telemetry/tracer\"\nimport { TransactionSerializer } from \"./TransactionSerializer\"\nimport type { OfflineTransaction, StorageAdapter } from \"../types\"\nimport type { Collection } from \"@tanstack/db\"\n\nexport class OutboxManager {\n private storage: StorageAdapter\n private serializer: TransactionSerializer\n private keyPrefix = `tx:`\n\n constructor(\n storage: StorageAdapter,\n collections: Record<string, Collection>\n ) {\n this.storage = storage\n this.serializer = new TransactionSerializer(collections)\n }\n\n private getStorageKey(id: string): string {\n return `${this.keyPrefix}${id}`\n }\n\n async add(transaction: OfflineTransaction): Promise<void> {\n return withSpan(\n `outbox.add`,\n {\n \"transaction.id\": transaction.id,\n \"transaction.mutationFnName\": transaction.mutationFnName,\n \"transaction.keyCount\": transaction.keys.length,\n },\n async () => {\n const key = this.getStorageKey(transaction.id)\n const serialized = this.serializer.serialize(transaction)\n await this.storage.set(key, serialized)\n }\n )\n }\n\n async get(id: string): Promise<OfflineTransaction | null> {\n return withSpan(`outbox.get`, { \"transaction.id\": id }, async (span) => {\n const key = this.getStorageKey(id)\n const data = await this.storage.get(key)\n\n if (!data) {\n span.setAttribute(`result`, `not_found`)\n return null\n }\n\n try {\n const transaction = this.serializer.deserialize(data)\n span.setAttribute(`result`, `found`)\n return transaction\n } catch (error) {\n console.warn(`Failed to deserialize transaction ${id}:`, error)\n span.setAttribute(`result`, `deserialize_error`)\n return null\n }\n })\n }\n\n async getAll(): Promise<Array<OfflineTransaction>> {\n return withSpan(`outbox.getAll`, {}, async (span) => {\n const keys = await this.storage.keys()\n const transactionKeys = keys.filter((key) =>\n key.startsWith(this.keyPrefix)\n )\n\n span.setAttribute(`transactionCount`, transactionKeys.length)\n\n const transactions: Array<OfflineTransaction> = []\n\n for (const key of transactionKeys) {\n const data = await this.storage.get(key)\n if (data) {\n try {\n const transaction = this.serializer.deserialize(data)\n transactions.push(transaction)\n } catch (error) {\n console.warn(\n `Failed to deserialize transaction from key ${key}:`,\n error\n )\n }\n }\n }\n\n return transactions.sort(\n (a, b) => a.createdAt.getTime() - b.createdAt.getTime()\n )\n })\n }\n\n async getByKeys(keys: Array<string>): Promise<Array<OfflineTransaction>> {\n const allTransactions = await this.getAll()\n const keySet = new Set(keys)\n\n return allTransactions.filter((transaction) =>\n transaction.keys.some((key) => keySet.has(key))\n )\n }\n\n async update(\n id: string,\n updates: Partial<OfflineTransaction>\n ): Promise<void> {\n return withSpan(`outbox.update`, { \"transaction.id\": id }, async () => {\n const existing = await this.get(id)\n if (!existing) {\n throw new Error(`Transaction ${id} not found`)\n }\n\n const updated = { ...existing, ...updates }\n await this.add(updated)\n })\n }\n\n async remove(id: string): Promise<void> {\n return withSpan(`outbox.remove`, { \"transaction.id\": id }, async () => {\n const key = this.getStorageKey(id)\n await this.storage.delete(key)\n })\n }\n\n async removeMany(ids: Array<string>): Promise<void> {\n return withSpan(`outbox.removeMany`, { count: ids.length }, async () => {\n await Promise.all(ids.map((id) => this.remove(id)))\n })\n }\n\n async clear(): Promise<void> {\n const keys = await this.storage.keys()\n const transactionKeys = keys.filter((key) => key.startsWith(this.keyPrefix))\n\n await Promise.all(transactionKeys.map((key) => this.storage.delete(key)))\n }\n\n async count(): Promise<number> {\n const keys = await this.storage.keys()\n return keys.filter((key) => key.startsWith(this.keyPrefix)).length\n }\n}\n"],"names":[],"mappings":";;AAKO,MAAM,cAAc;AAAA,EAKzB,YACE,SACA,aACA;AALF,SAAQ,YAAY;AAMlB,SAAK,UAAU;AACf,SAAK,aAAa,IAAI,sBAAsB,WAAW;AAAA,EACzD;AAAA,EAEQ,cAAc,IAAoB;AACxC,WAAO,GAAG,KAAK,SAAS,GAAG,EAAE;AAAA,EAC/B;AAAA,EAEA,MAAM,IAAI,aAAgD;AACxD,WAAO;AAAA,MACL;AAAA,MACA;AAAA,QACE,kBAAkB,YAAY;AAAA,QAC9B,8BAA8B,YAAY;AAAA,QAC1C,wBAAwB,YAAY,KAAK;AAAA,MAAA;AAAA,MAE3C,YAAY;AACV,cAAM,MAAM,KAAK,cAAc,YAAY,EAAE;AAC7C,cAAM,aAAa,KAAK,WAAW,UAAU,WAAW;AACxD,cAAM,KAAK,QAAQ,IAAI,KAAK,UAAU;AAAA,MACxC;AAAA,IAAA;AAAA,EAEJ;AAAA,EAEA,MAAM,IAAI,IAAgD;AACxD,WAAO,SAAS,cAAc,
|
|
1
|
+
{"version":3,"file":"OutboxManager.js","sources":["../../../src/outbox/OutboxManager.ts"],"sourcesContent":["import { withSpan } from \"../telemetry/tracer\"\nimport { TransactionSerializer } from \"./TransactionSerializer\"\nimport type { OfflineTransaction, StorageAdapter } from \"../types\"\nimport type { Collection } from \"@tanstack/db\"\n\nexport class OutboxManager {\n private storage: StorageAdapter\n private serializer: TransactionSerializer\n private keyPrefix = `tx:`\n\n constructor(\n storage: StorageAdapter,\n collections: Record<string, Collection>\n ) {\n this.storage = storage\n this.serializer = new TransactionSerializer(collections)\n }\n\n private getStorageKey(id: string): string {\n return `${this.keyPrefix}${id}`\n }\n\n async add(transaction: OfflineTransaction): Promise<void> {\n return withSpan(\n `outbox.add`,\n {\n \"transaction.id\": transaction.id,\n \"transaction.mutationFnName\": transaction.mutationFnName,\n \"transaction.keyCount\": transaction.keys.length,\n },\n async () => {\n const key = this.getStorageKey(transaction.id)\n const serialized = this.serializer.serialize(transaction)\n await this.storage.set(key, serialized)\n }\n )\n }\n\n async get(id: string): Promise<OfflineTransaction | null> {\n return withSpan(`outbox.get`, { \"transaction.id\": id }, async (span) => {\n const key = this.getStorageKey(id)\n const data = await this.storage.get(key)\n\n if (!data) {\n span.setAttribute(`result`, `not_found`)\n return null\n }\n\n try {\n const transaction = this.serializer.deserialize(data)\n span.setAttribute(`result`, `found`)\n return transaction\n } catch (error) {\n console.warn(`Failed to deserialize transaction ${id}:`, error)\n span.setAttribute(`result`, `deserialize_error`)\n return null\n }\n })\n }\n\n async getAll(): Promise<Array<OfflineTransaction>> {\n return withSpan(`outbox.getAll`, {}, async (span) => {\n const keys = await this.storage.keys()\n const transactionKeys = keys.filter((key) =>\n key.startsWith(this.keyPrefix)\n )\n\n span.setAttribute(`transactionCount`, transactionKeys.length)\n\n const transactions: Array<OfflineTransaction> = []\n\n for (const key of transactionKeys) {\n const data = await this.storage.get(key)\n if (data) {\n try {\n const transaction = this.serializer.deserialize(data)\n transactions.push(transaction)\n } catch (error) {\n console.warn(\n `Failed to deserialize transaction from key ${key}:`,\n error\n )\n }\n }\n }\n\n return transactions.sort(\n (a, b) => a.createdAt.getTime() - b.createdAt.getTime()\n )\n })\n }\n\n async getByKeys(keys: Array<string>): Promise<Array<OfflineTransaction>> {\n const allTransactions = await this.getAll()\n const keySet = new Set(keys)\n\n return allTransactions.filter((transaction) =>\n transaction.keys.some((key) => keySet.has(key))\n )\n }\n\n async update(\n id: string,\n updates: Partial<OfflineTransaction>\n ): Promise<void> {\n return withSpan(`outbox.update`, { \"transaction.id\": id }, async () => {\n const existing = await this.get(id)\n if (!existing) {\n throw new Error(`Transaction ${id} not found`)\n }\n\n const updated = { ...existing, ...updates }\n await this.add(updated)\n })\n }\n\n async remove(id: string): Promise<void> {\n return withSpan(`outbox.remove`, { \"transaction.id\": id }, async () => {\n const key = this.getStorageKey(id)\n await this.storage.delete(key)\n })\n }\n\n async removeMany(ids: Array<string>): Promise<void> {\n return withSpan(`outbox.removeMany`, { count: ids.length }, async () => {\n await Promise.all(ids.map((id) => this.remove(id)))\n })\n }\n\n async clear(): Promise<void> {\n const keys = await this.storage.keys()\n const transactionKeys = keys.filter((key) => key.startsWith(this.keyPrefix))\n\n await Promise.all(transactionKeys.map((key) => this.storage.delete(key)))\n }\n\n async count(): Promise<number> {\n const keys = await this.storage.keys()\n return keys.filter((key) => key.startsWith(this.keyPrefix)).length\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,29 +1,30 @@
|
|
|
1
|
-
import { Span, SpanContext } from '@opentelemetry/api';
|
|
2
1
|
export interface SpanAttrs {
|
|
3
2
|
[key: string]: string | number | boolean | undefined;
|
|
4
3
|
}
|
|
5
4
|
interface WithSpanOptions {
|
|
6
|
-
parentContext?:
|
|
5
|
+
parentContext?: any;
|
|
7
6
|
}
|
|
8
7
|
/**
|
|
9
8
|
* Lightweight span wrapper with error handling.
|
|
10
|
-
*
|
|
9
|
+
* No-op implementation - telemetry has been removed.
|
|
11
10
|
*
|
|
12
11
|
* By default, creates spans at the current context level (siblings).
|
|
13
12
|
* Use withNestedSpan if you want parent-child relationships.
|
|
14
13
|
*/
|
|
15
|
-
export declare function withSpan<T>(name: string, attrs: SpanAttrs, fn: (span:
|
|
14
|
+
export declare function withSpan<T>(name: string, attrs: SpanAttrs, fn: (span: any) => Promise<T>, _options?: WithSpanOptions): Promise<T>;
|
|
16
15
|
/**
|
|
17
16
|
* Like withSpan but propagates context so child spans nest properly.
|
|
18
|
-
*
|
|
17
|
+
* No-op implementation - telemetry has been removed.
|
|
19
18
|
*/
|
|
20
|
-
export declare function withNestedSpan<T>(name: string, attrs: SpanAttrs, fn: (span:
|
|
19
|
+
export declare function withNestedSpan<T>(name: string, attrs: SpanAttrs, fn: (span: any) => Promise<T>, _options?: WithSpanOptions): Promise<T>;
|
|
21
20
|
/**
|
|
22
21
|
* Creates a synchronous span for non-async operations
|
|
22
|
+
* No-op implementation - telemetry has been removed.
|
|
23
23
|
*/
|
|
24
|
-
export declare function withSyncSpan<T>(name: string, attrs: SpanAttrs, fn: (span:
|
|
24
|
+
export declare function withSyncSpan<T>(name: string, attrs: SpanAttrs, fn: (span: any) => T, _options?: WithSpanOptions): T;
|
|
25
25
|
/**
|
|
26
26
|
* Get the current tracer instance
|
|
27
|
+
* No-op implementation - telemetry has been removed.
|
|
27
28
|
*/
|
|
28
|
-
export declare function getTracer():
|
|
29
|
+
export declare function getTracer(): null;
|
|
29
30
|
export {};
|
|
@@ -1,87 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const parentCtx = getParentContext(options);
|
|
12
|
-
const span = TRACER.startSpan(name, void 0, parentCtx);
|
|
13
|
-
const filteredAttrs = {};
|
|
14
|
-
for (const [key, value] of Object.entries(attrs)) {
|
|
15
|
-
if (value !== void 0) {
|
|
16
|
-
filteredAttrs[key] = value;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
span.setAttributes(filteredAttrs);
|
|
20
|
-
try {
|
|
21
|
-
const result = await fn(span);
|
|
22
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
23
|
-
return result;
|
|
24
|
-
} catch (error) {
|
|
25
|
-
span.setStatus({
|
|
26
|
-
code: SpanStatusCode.ERROR,
|
|
27
|
-
message: error instanceof Error ? error.message : String(error)
|
|
28
|
-
});
|
|
29
|
-
span.recordException(error);
|
|
30
|
-
throw error;
|
|
31
|
-
} finally {
|
|
32
|
-
span.end();
|
|
1
|
+
const noopSpan = {
|
|
2
|
+
setAttribute: () => {
|
|
3
|
+
},
|
|
4
|
+
setAttributes: () => {
|
|
5
|
+
},
|
|
6
|
+
setStatus: () => {
|
|
7
|
+
},
|
|
8
|
+
recordException: () => {
|
|
9
|
+
},
|
|
10
|
+
end: () => {
|
|
33
11
|
}
|
|
12
|
+
};
|
|
13
|
+
async function withSpan(name, attrs, fn, _options) {
|
|
14
|
+
return await fn(noopSpan);
|
|
34
15
|
}
|
|
35
|
-
async function withNestedSpan(name, attrs, fn,
|
|
36
|
-
|
|
37
|
-
const span = TRACER.startSpan(name, void 0, parentCtx);
|
|
38
|
-
const filteredAttrs = {};
|
|
39
|
-
for (const [key, value] of Object.entries(attrs)) {
|
|
40
|
-
if (value !== void 0) {
|
|
41
|
-
filteredAttrs[key] = value;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
span.setAttributes(filteredAttrs);
|
|
45
|
-
const ctx = trace.setSpan(parentCtx, span);
|
|
46
|
-
try {
|
|
47
|
-
const result = await context.with(ctx, () => fn(span));
|
|
48
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
49
|
-
return result;
|
|
50
|
-
} catch (error) {
|
|
51
|
-
span.setStatus({
|
|
52
|
-
code: SpanStatusCode.ERROR,
|
|
53
|
-
message: error instanceof Error ? error.message : String(error)
|
|
54
|
-
});
|
|
55
|
-
span.recordException(error);
|
|
56
|
-
throw error;
|
|
57
|
-
} finally {
|
|
58
|
-
span.end();
|
|
59
|
-
}
|
|
16
|
+
async function withNestedSpan(name, attrs, fn, _options) {
|
|
17
|
+
return await fn(noopSpan);
|
|
60
18
|
}
|
|
61
|
-
function withSyncSpan(name, attrs, fn,
|
|
62
|
-
|
|
63
|
-
const span = TRACER.startSpan(name, void 0, parentCtx);
|
|
64
|
-
const filteredAttrs = {};
|
|
65
|
-
for (const [key, value] of Object.entries(attrs)) {
|
|
66
|
-
if (value !== void 0) {
|
|
67
|
-
filteredAttrs[key] = value;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
span.setAttributes(filteredAttrs);
|
|
71
|
-
try {
|
|
72
|
-
const result = fn(span);
|
|
73
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
74
|
-
return result;
|
|
75
|
-
} catch (error) {
|
|
76
|
-
span.setStatus({
|
|
77
|
-
code: SpanStatusCode.ERROR,
|
|
78
|
-
message: error instanceof Error ? error.message : String(error)
|
|
79
|
-
});
|
|
80
|
-
span.recordException(error);
|
|
81
|
-
throw error;
|
|
82
|
-
} finally {
|
|
83
|
-
span.end();
|
|
84
|
-
}
|
|
19
|
+
function withSyncSpan(name, attrs, fn, _options) {
|
|
20
|
+
return fn(noopSpan);
|
|
85
21
|
}
|
|
86
22
|
export {
|
|
87
23
|
withNestedSpan,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracer.js","sources":["../../../src/telemetry/tracer.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"tracer.js","sources":["../../../src/telemetry/tracer.ts"],"sourcesContent":["export interface SpanAttrs {\n [key: string]: string | number | boolean | undefined\n}\n\ninterface WithSpanOptions {\n parentContext?: any\n}\n\n// No-op span implementation\nconst noopSpan = {\n setAttribute: () => {},\n setAttributes: () => {},\n setStatus: () => {},\n recordException: () => {},\n end: () => {},\n}\n\n/**\n * Lightweight span wrapper with error handling.\n * No-op implementation - telemetry has been removed.\n *\n * By default, creates spans at the current context level (siblings).\n * Use withNestedSpan if you want parent-child relationships.\n */\nexport async function withSpan<T>(\n name: string,\n attrs: SpanAttrs,\n fn: (span: any) => Promise<T>,\n _options?: WithSpanOptions\n): Promise<T> {\n return await fn(noopSpan)\n}\n\n/**\n * Like withSpan but propagates context so child spans nest properly.\n * No-op implementation - telemetry has been removed.\n */\nexport async function withNestedSpan<T>(\n name: string,\n attrs: SpanAttrs,\n fn: (span: any) => Promise<T>,\n _options?: WithSpanOptions\n): Promise<T> {\n return await fn(noopSpan)\n}\n\n/**\n * Creates a synchronous span for non-async operations\n * No-op implementation - telemetry has been removed.\n */\nexport function withSyncSpan<T>(\n name: string,\n attrs: SpanAttrs,\n fn: (span: any) => T,\n _options?: WithSpanOptions\n): T {\n return fn(noopSpan)\n}\n\n/**\n * Get the current tracer instance\n * No-op implementation - telemetry has been removed.\n */\nexport function getTracer() {\n return null\n}\n"],"names":[],"mappings":"AASA,MAAM,WAAW;AAAA,EACf,cAAc,MAAM;AAAA,EAAC;AAAA,EACrB,eAAe,MAAM;AAAA,EAAC;AAAA,EACtB,WAAW,MAAM;AAAA,EAAC;AAAA,EAClB,iBAAiB,MAAM;AAAA,EAAC;AAAA,EACxB,KAAK,MAAM;AAAA,EAAC;AACd;AASA,eAAsB,SACpB,MACA,OACA,IACA,UACY;AACZ,SAAO,MAAM,GAAG,QAAQ;AAC1B;AAMA,eAAsB,eACpB,MACA,OACA,IACA,UACY;AACZ,SAAO,MAAM,GAAG,QAAQ;AAC1B;AAMO,SAAS,aACd,MACA,OACA,IACA,UACG;AACH,SAAO,GAAG,QAAQ;AACpB;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/offline-transactions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Offline-first transaction capabilities for TanStack DB",
|
|
5
5
|
"author": "TanStack",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,15 +42,13 @@
|
|
|
42
42
|
"dist",
|
|
43
43
|
"src"
|
|
44
44
|
],
|
|
45
|
-
"dependencies": {
|
|
46
|
-
"@opentelemetry/api": "^1.9.0",
|
|
47
|
-
"@tanstack/db": "0.4.19"
|
|
48
|
-
},
|
|
45
|
+
"dependencies": {},
|
|
49
46
|
"devDependencies": {
|
|
50
47
|
"@types/node": "^20.0.0",
|
|
51
48
|
"eslint": "^8.57.0",
|
|
52
49
|
"typescript": "^5.5.4",
|
|
53
|
-
"vitest": "^3.2.4"
|
|
50
|
+
"vitest": "^3.2.4",
|
|
51
|
+
"@tanstack/db": "0.4.20"
|
|
54
52
|
},
|
|
55
53
|
"peerDependencies": {
|
|
56
54
|
"@tanstack/db": "*"
|
package/src/api/OfflineAction.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { SpanStatusCode, context, trace } from "@opentelemetry/api"
|
|
2
1
|
import { OnMutateMustBeSynchronousError } from "@tanstack/db"
|
|
3
2
|
import { OfflineTransaction } from "./OfflineTransaction"
|
|
4
3
|
import type { Transaction } from "@tanstack/db"
|
|
@@ -45,30 +44,15 @@ export function createOfflineAction<T>(
|
|
|
45
44
|
}
|
|
46
45
|
})
|
|
47
46
|
|
|
48
|
-
// Immediately commit
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// Return the promise synchronously - this is critical for context propagation in browsers
|
|
58
|
-
return (async () => {
|
|
59
|
-
try {
|
|
60
|
-
await transaction.commit()
|
|
61
|
-
span.setStatus({ code: SpanStatusCode.OK })
|
|
62
|
-
span.end()
|
|
63
|
-
console.log(`ended offlineAction span - success`)
|
|
64
|
-
} catch (error) {
|
|
65
|
-
span.recordException(error as Error)
|
|
66
|
-
span.setStatus({ code: SpanStatusCode.ERROR })
|
|
67
|
-
span.end()
|
|
68
|
-
console.log(`ended offlineAction span - error`)
|
|
69
|
-
}
|
|
70
|
-
})()
|
|
71
|
-
})
|
|
47
|
+
// Immediately commit
|
|
48
|
+
const commitPromise = (async () => {
|
|
49
|
+
try {
|
|
50
|
+
await transaction.commit()
|
|
51
|
+
console.log(`offlineAction committed - success`)
|
|
52
|
+
} catch {
|
|
53
|
+
console.log(`offlineAction commit failed - error`)
|
|
54
|
+
}
|
|
55
|
+
})()
|
|
72
56
|
|
|
73
57
|
// Don't await - this is fire-and-forget for optimistic actions
|
|
74
58
|
// But catch to prevent unhandled rejection
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { context, trace } from "@opentelemetry/api"
|
|
2
1
|
import { createTransaction } from "@tanstack/db"
|
|
3
2
|
import { NonRetriableError } from "../types"
|
|
4
3
|
import type { PendingMutation, Transaction } from "@tanstack/db"
|
|
@@ -40,9 +39,6 @@ export class OfflineTransaction {
|
|
|
40
39
|
mutationFn: async () => {
|
|
41
40
|
// This is the blocking mutationFn that waits for the executor
|
|
42
41
|
// First persist the transaction to the outbox
|
|
43
|
-
const activeSpan = trace.getSpan(context.active())
|
|
44
|
-
const spanContext = activeSpan?.spanContext()
|
|
45
|
-
|
|
46
42
|
const offlineTransaction: OfflineTransactionType = {
|
|
47
43
|
id: this.offlineId,
|
|
48
44
|
mutationFnName: this.mutationFnName,
|
|
@@ -53,14 +49,7 @@ export class OfflineTransaction {
|
|
|
53
49
|
retryCount: 0,
|
|
54
50
|
nextAttemptAt: Date.now(),
|
|
55
51
|
metadata: this.metadata,
|
|
56
|
-
spanContext:
|
|
57
|
-
? {
|
|
58
|
-
traceId: spanContext.traceId,
|
|
59
|
-
spanId: spanContext.spanId,
|
|
60
|
-
traceFlags: spanContext.traceFlags,
|
|
61
|
-
traceState: spanContext.traceState?.serialize(),
|
|
62
|
-
}
|
|
63
|
-
: undefined,
|
|
52
|
+
spanContext: undefined,
|
|
64
53
|
version: 1,
|
|
65
54
|
}
|
|
66
55
|
|
|
@@ -1,35 +1,12 @@
|
|
|
1
|
-
import { createTraceState } from "@opentelemetry/api"
|
|
2
1
|
import { DefaultRetryPolicy } from "../retry/RetryPolicy"
|
|
3
2
|
import { NonRetriableError } from "../types"
|
|
4
3
|
import { withNestedSpan } from "../telemetry/tracer"
|
|
5
|
-
import type { SpanContext } from "@opentelemetry/api"
|
|
6
4
|
import type { KeyScheduler } from "./KeyScheduler"
|
|
7
5
|
import type { OutboxManager } from "../outbox/OutboxManager"
|
|
8
|
-
import type {
|
|
9
|
-
OfflineConfig,
|
|
10
|
-
OfflineTransaction,
|
|
11
|
-
SerializedSpanContext,
|
|
12
|
-
} from "../types"
|
|
6
|
+
import type { OfflineConfig, OfflineTransaction } from "../types"
|
|
13
7
|
|
|
14
8
|
const HANDLED_EXECUTION_ERROR = Symbol(`HandledExecutionError`)
|
|
15
9
|
|
|
16
|
-
function toSpanContext(
|
|
17
|
-
serialized?: SerializedSpanContext
|
|
18
|
-
): SpanContext | undefined {
|
|
19
|
-
if (!serialized) {
|
|
20
|
-
return undefined
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return {
|
|
24
|
-
traceId: serialized.traceId,
|
|
25
|
-
spanId: serialized.spanId,
|
|
26
|
-
traceFlags: serialized.traceFlags,
|
|
27
|
-
traceState: serialized.traceState
|
|
28
|
-
? createTraceState(serialized.traceState)
|
|
29
|
-
: undefined,
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
10
|
export class TransactionExecutor {
|
|
34
11
|
private scheduler: KeyScheduler
|
|
35
12
|
private outbox: OutboxManager
|
|
@@ -131,9 +108,6 @@ export class TransactionExecutor {
|
|
|
131
108
|
;(err as any)[HANDLED_EXECUTION_ERROR] = true
|
|
132
109
|
throw err
|
|
133
110
|
}
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
parentContext: toSpanContext(transaction.spanContext),
|
|
137
111
|
}
|
|
138
112
|
)
|
|
139
113
|
} catch (error) {
|
package/src/telemetry/tracer.ts
CHANGED
|
@@ -1,28 +1,23 @@
|
|
|
1
|
-
import { SpanStatusCode, context, trace } from "@opentelemetry/api"
|
|
2
|
-
import type { Span, SpanContext } from "@opentelemetry/api"
|
|
3
|
-
|
|
4
|
-
const TRACER = trace.getTracer(`@tanstack/offline-transactions`, `0.0.1`)
|
|
5
|
-
|
|
6
1
|
export interface SpanAttrs {
|
|
7
2
|
[key: string]: string | number | boolean | undefined
|
|
8
3
|
}
|
|
9
4
|
|
|
10
5
|
interface WithSpanOptions {
|
|
11
|
-
parentContext?:
|
|
6
|
+
parentContext?: any
|
|
12
7
|
}
|
|
13
8
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
9
|
+
// No-op span implementation
|
|
10
|
+
const noopSpan = {
|
|
11
|
+
setAttribute: () => {},
|
|
12
|
+
setAttributes: () => {},
|
|
13
|
+
setStatus: () => {},
|
|
14
|
+
recordException: () => {},
|
|
15
|
+
end: () => {},
|
|
21
16
|
}
|
|
22
17
|
|
|
23
18
|
/**
|
|
24
19
|
* Lightweight span wrapper with error handling.
|
|
25
|
-
*
|
|
20
|
+
* No-op implementation - telemetry has been removed.
|
|
26
21
|
*
|
|
27
22
|
* By default, creates spans at the current context level (siblings).
|
|
28
23
|
* Use withNestedSpan if you want parent-child relationships.
|
|
@@ -30,122 +25,42 @@ function getParentContext(options?: WithSpanOptions) {
|
|
|
30
25
|
export async function withSpan<T>(
|
|
31
26
|
name: string,
|
|
32
27
|
attrs: SpanAttrs,
|
|
33
|
-
fn: (span:
|
|
34
|
-
|
|
28
|
+
fn: (span: any) => Promise<T>,
|
|
29
|
+
_options?: WithSpanOptions
|
|
35
30
|
): Promise<T> {
|
|
36
|
-
|
|
37
|
-
const span = TRACER.startSpan(name, undefined, parentCtx)
|
|
38
|
-
|
|
39
|
-
// Filter out undefined attributes
|
|
40
|
-
const filteredAttrs: Record<string, string | number | boolean> = {}
|
|
41
|
-
for (const [key, value] of Object.entries(attrs)) {
|
|
42
|
-
if (value !== undefined) {
|
|
43
|
-
filteredAttrs[key] = value
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
span.setAttributes(filteredAttrs)
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
const result = await fn(span)
|
|
51
|
-
span.setStatus({ code: SpanStatusCode.OK })
|
|
52
|
-
return result
|
|
53
|
-
} catch (error) {
|
|
54
|
-
span.setStatus({
|
|
55
|
-
code: SpanStatusCode.ERROR,
|
|
56
|
-
message: error instanceof Error ? error.message : String(error),
|
|
57
|
-
})
|
|
58
|
-
span.recordException(error as Error)
|
|
59
|
-
throw error
|
|
60
|
-
} finally {
|
|
61
|
-
span.end()
|
|
62
|
-
}
|
|
31
|
+
return await fn(noopSpan)
|
|
63
32
|
}
|
|
64
33
|
|
|
65
34
|
/**
|
|
66
35
|
* Like withSpan but propagates context so child spans nest properly.
|
|
67
|
-
*
|
|
36
|
+
* No-op implementation - telemetry has been removed.
|
|
68
37
|
*/
|
|
69
38
|
export async function withNestedSpan<T>(
|
|
70
39
|
name: string,
|
|
71
40
|
attrs: SpanAttrs,
|
|
72
|
-
fn: (span:
|
|
73
|
-
|
|
41
|
+
fn: (span: any) => Promise<T>,
|
|
42
|
+
_options?: WithSpanOptions
|
|
74
43
|
): Promise<T> {
|
|
75
|
-
|
|
76
|
-
const span = TRACER.startSpan(name, undefined, parentCtx)
|
|
77
|
-
|
|
78
|
-
// Filter out undefined attributes
|
|
79
|
-
const filteredAttrs: Record<string, string | number | boolean> = {}
|
|
80
|
-
for (const [key, value] of Object.entries(attrs)) {
|
|
81
|
-
if (value !== undefined) {
|
|
82
|
-
filteredAttrs[key] = value
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
span.setAttributes(filteredAttrs)
|
|
87
|
-
|
|
88
|
-
// Set the span as active context so child spans nest properly
|
|
89
|
-
const ctx = trace.setSpan(parentCtx, span)
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
// Execute the function within the span's context
|
|
93
|
-
const result = await context.with(ctx, () => fn(span))
|
|
94
|
-
span.setStatus({ code: SpanStatusCode.OK })
|
|
95
|
-
return result
|
|
96
|
-
} catch (error) {
|
|
97
|
-
span.setStatus({
|
|
98
|
-
code: SpanStatusCode.ERROR,
|
|
99
|
-
message: error instanceof Error ? error.message : String(error),
|
|
100
|
-
})
|
|
101
|
-
span.recordException(error as Error)
|
|
102
|
-
throw error
|
|
103
|
-
} finally {
|
|
104
|
-
span.end()
|
|
105
|
-
}
|
|
44
|
+
return await fn(noopSpan)
|
|
106
45
|
}
|
|
107
46
|
|
|
108
47
|
/**
|
|
109
48
|
* Creates a synchronous span for non-async operations
|
|
49
|
+
* No-op implementation - telemetry has been removed.
|
|
110
50
|
*/
|
|
111
51
|
export function withSyncSpan<T>(
|
|
112
52
|
name: string,
|
|
113
53
|
attrs: SpanAttrs,
|
|
114
|
-
fn: (span:
|
|
115
|
-
|
|
54
|
+
fn: (span: any) => T,
|
|
55
|
+
_options?: WithSpanOptions
|
|
116
56
|
): T {
|
|
117
|
-
|
|
118
|
-
const span = TRACER.startSpan(name, undefined, parentCtx)
|
|
119
|
-
|
|
120
|
-
// Filter out undefined attributes
|
|
121
|
-
const filteredAttrs: Record<string, string | number | boolean> = {}
|
|
122
|
-
for (const [key, value] of Object.entries(attrs)) {
|
|
123
|
-
if (value !== undefined) {
|
|
124
|
-
filteredAttrs[key] = value
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
span.setAttributes(filteredAttrs)
|
|
129
|
-
|
|
130
|
-
try {
|
|
131
|
-
const result = fn(span)
|
|
132
|
-
span.setStatus({ code: SpanStatusCode.OK })
|
|
133
|
-
return result
|
|
134
|
-
} catch (error) {
|
|
135
|
-
span.setStatus({
|
|
136
|
-
code: SpanStatusCode.ERROR,
|
|
137
|
-
message: error instanceof Error ? error.message : String(error),
|
|
138
|
-
})
|
|
139
|
-
span.recordException(error as Error)
|
|
140
|
-
throw error
|
|
141
|
-
} finally {
|
|
142
|
-
span.end()
|
|
143
|
-
}
|
|
57
|
+
return fn(noopSpan)
|
|
144
58
|
}
|
|
145
59
|
|
|
146
60
|
/**
|
|
147
61
|
* Get the current tracer instance
|
|
62
|
+
* No-op implementation - telemetry has been removed.
|
|
148
63
|
*/
|
|
149
64
|
export function getTracer() {
|
|
150
|
-
return
|
|
65
|
+
return null
|
|
151
66
|
}
|