Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .changeset/fix-dependency-bundling-issues.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
"@tanstack/offline-transactions": patch
"@tanstack/query-db-collection": patch
---

Fix dependency bundling issues by moving @tanstack/db to peerDependencies

**What Changed:**

Moved `@tanstack/db` from regular dependencies to peerDependencies in:

- `@tanstack/offline-transactions`
- `@tanstack/query-db-collection`

Removed `@opentelemetry/api` dependency from `@tanstack/offline-transactions`.

**Why:**

These extension packages incorrectly declared `@tanstack/db` as both a regular dependency AND a peerDependency simultaneously. This caused lock files to develop conflicting versions, resulting in multiple instances of `@tanstack/db` being installed in consuming applications.

The fix removes `@tanstack/db` from regular dependencies and keeps it only as a peerDependency. This ensures only one version of `@tanstack/db` is installed in the dependency tree, preventing version conflicts.

For local development, `@tanstack/db` remains in devDependencies so the packages can be built and tested independently.
6 changes: 2 additions & 4 deletions packages/offline-transactions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,10 @@
"typecheck": "tsc --noEmit",
"lint": "eslint src"
},
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"@tanstack/db": "workspace:*"
},
"dependencies": {},
"devDependencies": {
"@types/node": "^20.0.0",
"@tanstack/db": "workspace:*",
"eslint": "^8.57.0",
"typescript": "^5.5.4",
"vitest": "^3.2.4"
Expand Down
34 changes: 9 additions & 25 deletions packages/offline-transactions/src/api/OfflineAction.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { SpanStatusCode, context, trace } from "@opentelemetry/api"
import { OnMutateMustBeSynchronousError } from "@tanstack/db"
import { OfflineTransaction } from "./OfflineTransaction"
import type { Transaction } from "@tanstack/db"
Expand Down Expand Up @@ -45,30 +44,15 @@ export function createOfflineAction<T>(
}
})

// Immediately commit with span instrumentation
const tracer = trace.getTracer(`@tanstack/offline-transactions`, `0.0.1`)
const span = tracer.startSpan(`offlineAction.${mutationFnName}`)
const ctx = trace.setSpan(context.active(), span)
console.log(`starting offlineAction span`, { tracer, span, ctx })

// Execute the commit within the span context
// The key is to return the promise synchronously from context.with() so context binds to it
const commitPromise = context.with(ctx, () => {
// Return the promise synchronously - this is critical for context propagation in browsers
return (async () => {
try {
await transaction.commit()
span.setStatus({ code: SpanStatusCode.OK })
span.end()
console.log(`ended offlineAction span - success`)
} catch (error) {
span.recordException(error as Error)
span.setStatus({ code: SpanStatusCode.ERROR })
span.end()
console.log(`ended offlineAction span - error`)
}
})()
})
// Immediately commit
const commitPromise = (async () => {
try {
await transaction.commit()
console.log(`offlineAction committed - success`)
} catch {
console.log(`offlineAction commit failed - error`)
}
})()

// Don't await - this is fire-and-forget for optimistic actions
// But catch to prevent unhandled rejection
Expand Down
13 changes: 1 addition & 12 deletions packages/offline-transactions/src/api/OfflineTransaction.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { context, trace } from "@opentelemetry/api"
import { createTransaction } from "@tanstack/db"
import { NonRetriableError } from "../types"
import type { PendingMutation, Transaction } from "@tanstack/db"
Expand Down Expand Up @@ -40,9 +39,6 @@ export class OfflineTransaction {
mutationFn: async () => {
// This is the blocking mutationFn that waits for the executor
// First persist the transaction to the outbox
const activeSpan = trace.getSpan(context.active())
const spanContext = activeSpan?.spanContext()

const offlineTransaction: OfflineTransactionType = {
id: this.offlineId,
mutationFnName: this.mutationFnName,
Expand All @@ -53,14 +49,7 @@ export class OfflineTransaction {
retryCount: 0,
nextAttemptAt: Date.now(),
metadata: this.metadata,
spanContext: spanContext
? {
traceId: spanContext.traceId,
spanId: spanContext.spanId,
traceFlags: spanContext.traceFlags,
traceState: spanContext.traceState?.serialize(),
}
: undefined,
spanContext: undefined,
version: 1,
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,12 @@
import { createTraceState } from "@opentelemetry/api"
import { DefaultRetryPolicy } from "../retry/RetryPolicy"
import { NonRetriableError } from "../types"
import { withNestedSpan } from "../telemetry/tracer"
import type { SpanContext } from "@opentelemetry/api"
import type { KeyScheduler } from "./KeyScheduler"
import type { OutboxManager } from "../outbox/OutboxManager"
import type {
OfflineConfig,
OfflineTransaction,
SerializedSpanContext,
} from "../types"
import type { OfflineConfig, OfflineTransaction } from "../types"

const HANDLED_EXECUTION_ERROR = Symbol(`HandledExecutionError`)

function toSpanContext(
serialized?: SerializedSpanContext
): SpanContext | undefined {
if (!serialized) {
return undefined
}

return {
traceId: serialized.traceId,
spanId: serialized.spanId,
traceFlags: serialized.traceFlags,
traceState: serialized.traceState
? createTraceState(serialized.traceState)
: undefined,
}
}

export class TransactionExecutor {
private scheduler: KeyScheduler
private outbox: OutboxManager
Expand Down Expand Up @@ -131,9 +108,6 @@ export class TransactionExecutor {
;(err as any)[HANDLED_EXECUTION_ERROR] = true
throw err
}
},
{
parentContext: toSpanContext(transaction.spanContext),
}
)
} catch (error) {
Expand Down
129 changes: 22 additions & 107 deletions packages/offline-transactions/src/telemetry/tracer.ts
Original file line number Diff line number Diff line change
@@ -1,151 +1,66 @@
import { SpanStatusCode, context, trace } from "@opentelemetry/api"
import type { Span, SpanContext } from "@opentelemetry/api"

const TRACER = trace.getTracer(`@tanstack/offline-transactions`, `0.0.1`)

export interface SpanAttrs {
[key: string]: string | number | boolean | undefined
}

interface WithSpanOptions {
parentContext?: SpanContext
parentContext?: any
}

function getParentContext(options?: WithSpanOptions) {
if (options?.parentContext) {
const parentSpan = trace.wrapSpanContext(options.parentContext)
return trace.setSpan(context.active(), parentSpan)
}

return context.active()
// No-op span implementation
const noopSpan = {
setAttribute: () => {},
setAttributes: () => {},
setStatus: () => {},
recordException: () => {},
end: () => {},
}

/**
* Lightweight span wrapper with error handling.
* Uses OpenTelemetry API which is no-op when tracing is disabled.
* No-op implementation - telemetry has been removed.
*
* By default, creates spans at the current context level (siblings).
* Use withNestedSpan if you want parent-child relationships.
*/
export async function withSpan<T>(
name: string,
attrs: SpanAttrs,
fn: (span: Span) => Promise<T>,
options?: WithSpanOptions
fn: (span: any) => Promise<T>,
_options?: WithSpanOptions
): Promise<T> {
const parentCtx = getParentContext(options)
const span = TRACER.startSpan(name, undefined, parentCtx)

// Filter out undefined attributes
const filteredAttrs: Record<string, string | number | boolean> = {}
for (const [key, value] of Object.entries(attrs)) {
if (value !== undefined) {
filteredAttrs[key] = value
}
}

span.setAttributes(filteredAttrs)

try {
const result = await fn(span)
span.setStatus({ code: SpanStatusCode.OK })
return result
} catch (error) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: error instanceof Error ? error.message : String(error),
})
span.recordException(error as Error)
throw error
} finally {
span.end()
}
return await fn(noopSpan)
}

/**
* Like withSpan but propagates context so child spans nest properly.
* Use this when you want operations inside fn to be child spans.
* No-op implementation - telemetry has been removed.
*/
export async function withNestedSpan<T>(
name: string,
attrs: SpanAttrs,
fn: (span: Span) => Promise<T>,
options?: WithSpanOptions
fn: (span: any) => Promise<T>,
_options?: WithSpanOptions
): Promise<T> {
const parentCtx = getParentContext(options)
const span = TRACER.startSpan(name, undefined, parentCtx)

// Filter out undefined attributes
const filteredAttrs: Record<string, string | number | boolean> = {}
for (const [key, value] of Object.entries(attrs)) {
if (value !== undefined) {
filteredAttrs[key] = value
}
}

span.setAttributes(filteredAttrs)

// Set the span as active context so child spans nest properly
const ctx = trace.setSpan(parentCtx, span)

try {
// Execute the function within the span's context
const result = await context.with(ctx, () => fn(span))
span.setStatus({ code: SpanStatusCode.OK })
return result
} catch (error) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: error instanceof Error ? error.message : String(error),
})
span.recordException(error as Error)
throw error
} finally {
span.end()
}
return await fn(noopSpan)
}

/**
* Creates a synchronous span for non-async operations
* No-op implementation - telemetry has been removed.
*/
export function withSyncSpan<T>(
name: string,
attrs: SpanAttrs,
fn: (span: Span) => T,
options?: WithSpanOptions
fn: (span: any) => T,
_options?: WithSpanOptions
): T {
const parentCtx = getParentContext(options)
const span = TRACER.startSpan(name, undefined, parentCtx)

// Filter out undefined attributes
const filteredAttrs: Record<string, string | number | boolean> = {}
for (const [key, value] of Object.entries(attrs)) {
if (value !== undefined) {
filteredAttrs[key] = value
}
}

span.setAttributes(filteredAttrs)

try {
const result = fn(span)
span.setStatus({ code: SpanStatusCode.OK })
return result
} catch (error) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: error instanceof Error ? error.message : String(error),
})
span.recordException(error as Error)
throw error
} finally {
span.end()
}
return fn(noopSpan)
}

/**
* Get the current tracer instance
* No-op implementation - telemetry has been removed.
*/
export function getTracer() {
return TRACER
return null
}
5 changes: 3 additions & 2 deletions packages/query-db-collection/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
"description": "TanStack Query collection for TanStack DB",
"version": "0.2.42",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@tanstack/db": "workspace:*"
"@standard-schema/spec": "^1.0.0"
},
"devDependencies": {
"@tanstack/db": "workspace:*",
"@tanstack/query-core": "^5.90.5",
"@vitest/coverage-istanbul": "^3.2.4"
},
Expand All @@ -31,6 +31,7 @@
"module": "dist/esm/index.js",
"packageManager": "pnpm@10.19.0",
"peerDependencies": {
"@tanstack/db": "*",
"@tanstack/query-core": "^5.0.0",
"typescript": ">=4.7"
},
Expand Down
Loading
Loading