snapshot of https://github.com/TanStack/query.git (main, 2026-09-16; blobs > 131072B omitted)
Some checks failed
autofix.ci / autofix (push) Failing after 4s
Release / Release (push) Has been skipped
GitHub Actions Security Analysis / Run zizmor (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-16 01:13:08 +05:00
commit 3a013ccdfe
3570 changed files with 490194 additions and 0 deletions

View File

@@ -0,0 +1,452 @@
# @tanstack/query-broadcast-client-experimental
## 5.102.8
### Patch Changes
- Updated dependencies []:
- @tanstack/query-core@5.102.8
## 5.102.7
### Patch Changes
- Updated dependencies []:
- @tanstack/query-core@5.102.7
## 5.102.6
### Patch Changes
- Updated dependencies []:
- @tanstack/query-core@5.102.6
## 5.102.5
### Patch Changes
- Updated dependencies [[`578e5c2`](https://github.com/TanStack/query/commit/578e5c26e8ebd0d7351b4b8e2bafba695e672b8d)]:
- @tanstack/query-core@5.102.5
## 5.102.4
### Patch Changes
- Updated dependencies [[`a05df6a`](https://github.com/TanStack/query/commit/a05df6aefb0e2489ec2c879ae16e2ee7cb3123ec)]:
- @tanstack/query-core@5.102.4
## 5.102.3
### Patch Changes
- Updated dependencies []:
- @tanstack/query-core@5.102.3
## 5.102.2
### Patch Changes
- Updated dependencies [[`80fbf73`](https://github.com/TanStack/query/commit/80fbf73e77892d702c107e14a84c219a8ed825dc)]:
- @tanstack/query-core@5.102.2
## 5.102.1
### Patch Changes
- Updated dependencies [[`134890d`](https://github.com/TanStack/query/commit/134890dbdc60e4fb0313e44b512d29284ca82f96)]:
- @tanstack/query-core@5.102.1
## 5.102.0
### Patch Changes
- [#11242](https://github.com/TanStack/query/pull/11242) [`6796c51`](https://github.com/TanStack/query/commit/6796c512752ac95b1b593df91b20d5d416c1b69f) - fix(broadcast-client): recover from errors thrown while applying an incoming cross-tab message instead of permanently disabling this tab's own broadcasts
- [#10771](https://github.com/TanStack/query/pull/10771) [`3c79861`](https://github.com/TanStack/query/commit/3c79861ab1bf00152ed1f8ae03934d90a131dcd1) - Fix unhandled `DataCloneError` rejections in `broadcastQueryClient` when `postMessage` fails due to non-cloneable query data (e.g. `ReadableStream`, `Response`, Vue reactive proxies). Adds an optional `onBroadcastError` callback to handle errors explicitly; falls back to `console.warn` in development when not provided.
- Updated dependencies [[`34f7cee`](https://github.com/TanStack/query/commit/34f7ceed09c10e4a3aa2df31a106ddf02ec4e787), [`b4368c4`](https://github.com/TanStack/query/commit/b4368c43792349f6c29d1fb41f7ee1ef3a8bdd2c), [`5bb089d`](https://github.com/TanStack/query/commit/5bb089d153d33933983b7ea80f8d69b51d423698), [`ba4650c`](https://github.com/TanStack/query/commit/ba4650c05e61e33c609e051b948c9f7d31ce70d1), [`294d4e6`](https://github.com/TanStack/query/commit/294d4e62c4f7d674574a4903eef2a1bc3dd1413b), [`1f631b3`](https://github.com/TanStack/query/commit/1f631b3604aab6567fb3f6c90646a5a304641546), [`01a02bf`](https://github.com/TanStack/query/commit/01a02bfad32f9efbc17796be31d6399f1197a655), [`18c1c1e`](https://github.com/TanStack/query/commit/18c1c1ef94883e781ad36d36f9ea4a043ce4260b), [`5448063`](https://github.com/TanStack/query/commit/5448063f828d2270dccd41ae375e1afde35e91f2), [`2215bb0`](https://github.com/TanStack/query/commit/2215bb031139cdc8a84751b37a485c38ca9d2b6e), [`1ef4208`](https://github.com/TanStack/query/commit/1ef42087c9a266c2137d2ef645cbcc662f60ac93), [`5981771`](https://github.com/TanStack/query/commit/5981771abec9330b344a2617543eb05ba7c99e24), [`4a9bef6`](https://github.com/TanStack/query/commit/4a9bef6cf19b1cd6b014032d696129f12a848185), [`bef4bc7`](https://github.com/TanStack/query/commit/bef4bc780ce7cca32b7e3dea85f77d92f82a62a2), [`9656dc4`](https://github.com/TanStack/query/commit/9656dc4e5fef5f8c502579b52459f9e8c72e787c), [`326aaf1`](https://github.com/TanStack/query/commit/326aaf1333e5d9cbc46569c53f218d31684162d2), [`3e83601`](https://github.com/TanStack/query/commit/3e836010032c2586e9f1c66a271fa9114a6401f9), [`c6fc17c`](https://github.com/TanStack/query/commit/c6fc17cfad6e452261c585fabfb8c210e60531ed)]:
- @tanstack/query-core@5.102.0
## 5.101.4
### Patch Changes
- Updated dependencies []:
- @tanstack/query-core@5.101.4
## 5.101.3
### Patch Changes
- Updated dependencies [[`7e3c822`](https://github.com/TanStack/query/commit/7e3c822a10896f41a8f1031c16b85096277af677)]:
- @tanstack/query-core@5.101.3
## 5.101.2
### Patch Changes
- Updated dependencies []:
- @tanstack/query-core@5.101.2
## 5.101.1
### Patch Changes
- Updated dependencies [[`9eff92e`](https://github.com/TanStack/query/commit/9eff92ed86e284ec0125b3a3539d028688235bd1)]:
- @tanstack/query-core@5.101.1
## 5.101.0
### Patch Changes
- Updated dependencies []:
- @tanstack/query-core@5.101.0
## 5.100.14
### Patch Changes
- Updated dependencies []:
- @tanstack/query-core@5.100.14
## 5.100.13
### Patch Changes
- Updated dependencies [[`d423168`](https://github.com/TanStack/query/commit/d423168f6261a5cb3d353e53b27c8150cc271151)]:
- @tanstack/query-core@5.100.13
## 5.100.12
### Patch Changes
- Updated dependencies []:
- @tanstack/query-core@5.100.12
## 5.100.11
### Patch Changes
- Updated dependencies []:
- @tanstack/query-core@5.100.11
## 5.100.10
### Patch Changes
- Updated dependencies []:
- @tanstack/query-core@5.100.10
## 5.100.9
### Patch Changes
- Updated dependencies [[`fcee7bd`](https://github.com/TanStack/query/commit/fcee7bdc429385ae8ffa224fa8a7a9ec7b8ee380)]:
- @tanstack/query-core@5.100.9
## 5.100.8
### Patch Changes
- Updated dependencies []:
- @tanstack/query-core@5.100.8
## 5.100.7
### Patch Changes
- Updated dependencies []:
- @tanstack/query-core@5.100.7
## 5.100.6
### Patch Changes
- Updated dependencies []:
- @tanstack/query-core@5.100.6
## 5.100.5
### Patch Changes
- Updated dependencies [[`a53ef97`](https://github.com/TanStack/query/commit/a53ef97f87decb8ea2431710f5199431d3c94c8d)]:
- @tanstack/query-core@5.100.5
## 5.100.4
### Patch Changes
- Updated dependencies []:
- @tanstack/query-core@5.100.4
## 5.100.3
### Patch Changes
- Updated dependencies [[`f85d825`](https://github.com/TanStack/query/commit/f85d825e02efbbff02e2081528ed28f5e5382f7a)]:
- @tanstack/query-core@5.100.3
## 5.100.2
### Patch Changes
- Updated dependencies [[`ea4497e`](https://github.com/TanStack/query/commit/ea4497e8aa00d8c1c3a36fb1e17563a889d6ab31), [`d6a7bf3`](https://github.com/TanStack/query/commit/d6a7bf3e3e024c1a77d0536813238cc8007a5fa7), [`645d5d1`](https://github.com/TanStack/query/commit/645d5d130f5e8017cb1bf1a37987f7b980aed705)]:
- @tanstack/query-core@5.100.2
## 5.100.1
### Patch Changes
- Updated dependencies [[`1bb0d23`](https://github.com/TanStack/query/commit/1bb0d234280fd4ae1725c439088426a20593a8df)]:
- @tanstack/query-core@5.100.1
## 5.100.0
### Patch Changes
- Updated dependencies [[`6540a41`](https://github.com/TanStack/query/commit/6540a4126b1c087d86d64525e78f32d9920dcd31)]:
- @tanstack/query-core@5.100.0
## 5.99.2
### Patch Changes
- Updated dependencies []:
- @tanstack/query-core@5.99.2
## 5.99.1
### Patch Changes
- Updated dependencies []:
- @tanstack/query-core@5.99.1
## 5.99.0
### Patch Changes
- Updated dependencies []:
- @tanstack/query-core@5.99.0
## 5.98.0
### Patch Changes
- Updated dependencies []:
- @tanstack/query-core@5.98.0
## 5.97.0
### Patch Changes
- Updated dependencies [[`2bfb12c`](https://github.com/TanStack/query/commit/2bfb12cc44f1d8495106136e4ddacb817135f8f9)]:
- @tanstack/query-core@5.97.0
## 5.96.2
### Patch Changes
- Updated dependencies []:
- @tanstack/query-core@5.96.2
## 5.96.1
### Patch Changes
- Updated dependencies []:
- @tanstack/query-core@5.96.1
## 5.96.0
### Patch Changes
- Updated dependencies []:
- @tanstack/query-core@5.96.0
## 5.95.2
### Patch Changes
- Updated dependencies [[`cd5a35b`](https://github.com/TanStack/query/commit/cd5a35b328837781aa4f9305bb2bd7877ca934e9)]:
- @tanstack/query-core@5.95.2
## 5.95.1
### Patch Changes
- Updated dependencies [[`1f1775c`](https://github.com/TanStack/query/commit/1f1775ca92f2b6c035682947ff3b3424804ff31a)]:
- @tanstack/query-core@5.95.1
## 5.95.0
### Patch Changes
- Updated dependencies []:
- @tanstack/query-core@5.95.0
## 5.94.5
### Patch Changes
- fix(\*): resolve issue about excluded build directory ([#10312](https://github.com/TanStack/query/pull/10312))
- Updated dependencies [[`4b6536d`](https://github.com/TanStack/query/commit/4b6536dfce99036f4e37f52943c6fed3ad0e0a18)]:
- @tanstack/query-core@5.94.5
## 5.94.4
### Patch Changes
- chore: fixed version ([#10064](https://github.com/TanStack/query/pull/10064))
- Updated dependencies [[`4c75210`](https://github.com/TanStack/query/commit/4c75210ce8235fe3d39b67e1029eff11278927cc)]:
- @tanstack/query-core@5.94.4
## 5.90.23
### Patch Changes
- fix(streamedQuery): maintain error state on reset refetch with initialData defined ([#10287](https://github.com/TanStack/query/pull/10287))
- Updated dependencies [[`248975e`](https://github.com/TanStack/query/commit/248975e896f585f6eaa505c796e73fcf7bfd1eec)]:
- @tanstack/query-core@5.91.2
## 5.90.22
### Patch Changes
- Updated dependencies [[`a89aab9`](https://github.com/TanStack/query/commit/a89aab975581c25c113a26c8af486b4cafad272a)]:
- @tanstack/query-core@5.91.1
## 5.90.21
### Patch Changes
- Updated dependencies [[`6fa901b`](https://github.com/TanStack/query/commit/6fa901b97a22a80d0fca3f6ed86237ff0cbdd13b)]:
- @tanstack/query-core@5.91.0
## 5.90.20
### Patch Changes
- Updated dependencies [[`e7258c5`](https://github.com/TanStack/query/commit/e7258c5cb30cafa456cdb4e6bc75b43bf619954d)]:
- @tanstack/query-core@5.90.20
## 5.90.19
### Patch Changes
- Updated dependencies [[`53fc74e`](https://github.com/TanStack/query/commit/53fc74ebb16730bd3317f039a69c6821386bae93)]:
- @tanstack/query-core@5.90.19
## 5.90.18
### Patch Changes
- Updated dependencies [[`dea1614`](https://github.com/TanStack/query/commit/dea1614aaad5c572cf43cea54b64ac09dc4d5b41)]:
- @tanstack/query-core@5.90.18
## 5.90.17
### Patch Changes
- Updated dependencies [[`269351b`](https://github.com/TanStack/query/commit/269351b8ce4b4846da3d320ac5b850ee6aada0d6)]:
- @tanstack/query-core@5.90.17
## 5.90.16
### Patch Changes
- Updated dependencies [[`7f47906`](https://github.com/TanStack/query/commit/7f47906eaccc3f3aa5ce24b77a83bd7a620a237b)]:
- @tanstack/query-core@5.90.16
## 5.90.15
### Patch Changes
- Updated dependencies [[`fccef79`](https://github.com/TanStack/query/commit/fccef797d57d4a9566517bba87c8377f363920f2)]:
- @tanstack/query-core@5.90.15
## 5.90.14
### Patch Changes
- Updated dependencies [[`d576092`](https://github.com/TanStack/query/commit/d576092e2ece4ca3936add3eb0da5234c1d82ed4)]:
- @tanstack/query-core@5.90.14
## 5.90.13
### Patch Changes
- Updated dependencies [[`4a0a78a`](https://github.com/TanStack/query/commit/4a0a78afbc2432f8cb6828035965853fa98c86a0)]:
- @tanstack/query-core@5.90.13
## 5.90.12
### Patch Changes
- Updated dependencies [[`72d8ac5`](https://github.com/TanStack/query/commit/72d8ac5c592004b8f9c3ee086fcb9c3cd615ca05)]:
- @tanstack/query-core@5.90.12
## 5.90.11
### Patch Changes
- Updated dependencies [[`c01b150`](https://github.com/TanStack/query/commit/c01b150e3673e11d6533768529a5e6fe3ebee68c)]:
- @tanstack/query-core@5.90.11
## 5.90.10
### Patch Changes
- Updated dependencies [[`8e2e174`](https://github.com/TanStack/query/commit/8e2e174e9fd2e7b94cd232041e49c9d014d74e26), [`eb559a6`](https://github.com/TanStack/query/commit/eb559a66dc0d77dd46435f624fa64fc068bef9ae)]:
- @tanstack/query-core@5.90.10
## 5.90.9
### Patch Changes
- Updated dependencies [[`08b211f`](https://github.com/TanStack/query/commit/08b211f8aa475e05d2f13a36517fc556861ef962)]:
- @tanstack/query-core@5.90.9
## 5.90.8
### Patch Changes
- Updated dependencies [[`c0ec9fe`](https://github.com/TanStack/query/commit/c0ec9fe0d1426fe3f233adda3ebf23989ffaa110)]:
- @tanstack/query-core@5.90.8
## 5.90.7
### Patch Changes
- Updated dependencies [[`b4cd121`](https://github.com/TanStack/query/commit/b4cd121a39d07cefaa3a3411136d342cc54ce8fb)]:
- @tanstack/query-core@5.90.7
## 5.90.6
### Patch Changes
- Updated dependencies [[`1638c02`](https://github.com/TanStack/query/commit/1638c028df55648995d04431179904371a189772)]:
- @tanstack/query-core@5.90.6
## 5.90.5
### Patch Changes
- Updated dependencies [[`e42ddfe`](https://github.com/TanStack/query/commit/e42ddfe919f34f847ca101aeef162c69845f9a1e)]:
- @tanstack/query-core@5.90.5
## 5.90.4
### Patch Changes
- Updated dependencies [[`20ef922`](https://github.com/TanStack/query/commit/20ef922a0a7c3aee00150bf69123c338b0922922)]:
- @tanstack/query-core@5.90.4
## 5.90.3
### Patch Changes
- Updated dependencies [[`4e1c433`](https://github.com/TanStack/query/commit/4e1c4338a72f7384600bbda99e44bc1891695df4)]:
- @tanstack/query-core@5.90.3

View File

@@ -0,0 +1,5 @@
// @ts-check
import rootConfig from './root.eslint.config.js'
export default [...rootConfig]

View File

@@ -0,0 +1,67 @@
{
"name": "@tanstack/query-broadcast-client-experimental",
"version": "5.102.8",
"description": "An experimental plugin to for broadcasting the state of your queryClient between browser tabs/windows",
"author": "tannerlinsley",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/TanStack/query.git",
"directory": "packages/query-broadcast-client-experimental"
},
"homepage": "https://tanstack.com/query",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"scripts": {
"clean": "premove ./build ./coverage ./dist-ts ./.cache/test-types",
"compile": "tsc --project tsconfig.json",
"test:eslint": "eslint --concurrency=auto ./src",
"test:types": "npm-run-all --serial test:types:*",
"test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --project tsconfig.json --outDir .cache/test-types/ts56 --tsBuildInfoFile .cache/test-types/ts56.tsbuildinfo",
"test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --project tsconfig.json --outDir .cache/test-types/ts57 --tsBuildInfoFile .cache/test-types/ts57.tsbuildinfo",
"test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js --project tsconfig.json --outDir .cache/test-types/ts58 --tsBuildInfoFile .cache/test-types/ts58.tsbuildinfo",
"test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js --project tsconfig.json --outDir .cache/test-types/ts59 --tsBuildInfoFile .cache/test-types/ts59.tsbuildinfo",
"test:types:tscurrent": "tsc --project tsconfig.json --outDir .cache/test-types/tscurrent --tsBuildInfoFile .cache/test-types/tscurrent.tsbuildinfo",
"test:types:ts70": "node ../../node_modules/typescript70/lib/tsc.js --project tsconfig.json --outDir .cache/test-types/ts70 --tsBuildInfoFile .cache/test-types/ts70.tsbuildinfo",
"test:lib": "vitest",
"test:lib:dev": "pnpm run test:lib --watch",
"test:build": "publint --strict && attw --pack",
"build": "tsdown --tsconfig tsconfig.prod.json"
},
"type": "module",
"types": "build/legacy/index.d.ts",
"main": "build/legacy/index.cjs",
"module": "build/legacy/index.js",
"exports": {
".": {
"@tanstack/custom-condition": "./src/index.ts",
"import": {
"types": "./build/modern/index.d.ts",
"default": "./build/modern/index.js"
},
"require": {
"types": "./build/modern/index.d.cts",
"default": "./build/modern/index.cjs"
}
},
"./package.json": "./package.json"
},
"sideEffects": false,
"files": [
"build",
"src",
"!src/__tests__"
],
"dependencies": {
"@tanstack/query-core": "workspace:*",
"broadcast-channel": "^7.0.0"
},
"devDependencies": {
"@tanstack/query-test-utils": "workspace:*",
"@testing-library/react": "^16.1.0",
"@vitejs/plugin-react": "^4.3.4",
"npm-run-all2": "^5.0.0"
}
}

View File

@@ -0,0 +1 @@
../../eslint.config.js

View File

@@ -0,0 +1 @@
../../scripts/getTsdownConfig.js

View File

@@ -0,0 +1,352 @@
import { QueryClient } from '@tanstack/query-core'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { queryKey, sleep } from '@tanstack/query-test-utils'
import { broadcastQueryClient } from '..'
import type { BroadcastErrorEvent } from '..'
import type { QueryCache } from '@tanstack/query-core'
const mockPostMessage = vi.fn().mockResolvedValue(undefined)
const mockClose = vi.fn()
let lastCreatedChannel: { onmessage: ((action: any) => void) | null }
vi.mock('broadcast-channel', async (importOriginal) => {
const actual = await importOriginal()
return {
...(actual as object),
BroadcastChannel: class MockBroadcastChannel {
onmessage = null
postMessage = mockPostMessage
close = mockClose
constructor() {
lastCreatedChannel = this
}
},
}
})
describe('broadcastQueryClient', () => {
let queryClient: QueryClient
let queryCache: QueryCache
beforeEach(() => {
queryClient = new QueryClient()
queryCache = queryClient.getQueryCache()
mockPostMessage.mockResolvedValue(undefined)
mockClose.mockReset()
})
it('should subscribe to the query cache', () => {
broadcastQueryClient({
queryClient,
broadcastChannel: 'test_channel',
})
expect(queryCache.hasListeners()).toBe(true)
})
it('should not have any listeners after cleanup', () => {
const unsubscribe = broadcastQueryClient({
queryClient,
broadcastChannel: 'test_channel',
})
unsubscribe()
expect(queryCache.hasListeners()).toBe(false)
})
describe('incoming message handling', () => {
it('should keep broadcasting local changes after applying an incoming message throws', () => {
const remoteKey = queryKey()
const localKey = queryKey()
broadcastQueryClient({
queryClient,
broadcastChannel: 'test_channel',
})
// Simulate some other part of the app (e.g. another cache subscriber)
// throwing synchronously while an incoming cross-tab message is being
// applied locally.
const explodingUnsubscribe = queryCache.subscribe(() => {
throw new Error('boom from an unrelated cache listener')
})
expect(() => {
lastCreatedChannel.onmessage?.({
type: 'added',
queryHash: JSON.stringify(remoteKey),
queryKey: remoteKey,
state: { data: 1 },
})
}).toThrow('boom')
explodingUnsubscribe()
mockPostMessage.mockClear()
// A later local change must still be broadcast to other tabs, instead
// of being silently swallowed because the transaction flag got stuck.
queryClient.setQueryData(localKey, { value: 1 })
expect(mockPostMessage).toHaveBeenCalled()
})
it('should keep broadcasting local changes after query.setState throws for an existing query', () => {
const existingKey = queryKey()
const localKey = queryKey()
broadcastQueryClient({
queryClient,
broadcastChannel: 'test_channel',
})
// Populate the cache with an existing query so the incoming message
// below is applied via `query.setState` rather than `queryCache.build`.
queryClient.setQueryData(existingKey, { value: 0 })
const existingQuery = queryCache.find({ queryKey: existingKey })!
const explodingUnsubscribe = queryCache.subscribe(() => {
throw new Error('boom from an unrelated cache listener')
})
expect(() => {
lastCreatedChannel.onmessage?.({
type: 'updated',
queryHash: existingQuery.queryHash,
queryKey: existingKey,
state: { data: 2 },
})
}).toThrow('boom')
explodingUnsubscribe()
mockPostMessage.mockClear()
queryClient.setQueryData(localKey, { value: 1 })
expect(mockPostMessage).toHaveBeenCalled()
})
})
describe('postMessage error handling', () => {
let originalEnv: string | undefined
beforeEach(() => {
originalEnv = process.env['NODE_ENV']
})
afterEach(() => {
process.env['NODE_ENV'] = originalEnv
})
it('should not cause an unhandled rejection when onBroadcastError itself throws', async () => {
const key = queryKey()
const cloneError = new DOMException('DataCloneError', 'DataCloneError')
mockPostMessage.mockRejectedValueOnce(cloneError)
const unhandledRejections: Array<unknown> = []
const onUnhandledRejection = (reason: unknown) => {
unhandledRejections.push(reason)
}
process.on('unhandledRejection', onUnhandledRejection)
try {
const onBroadcastError = vi.fn().mockImplementation(() => {
throw new Error('boom')
})
broadcastQueryClient({
queryClient,
broadcastChannel: 'test_channel',
onBroadcastError,
})
queryClient.setQueryData(key, { value: 1 })
await sleep(0)
expect(onBroadcastError).toHaveBeenCalledWith(
cloneError,
expect.objectContaining<BroadcastErrorEvent>({
type: 'added',
queryHash: expect.any(String) as string,
queryKey: key,
}),
)
expect(unhandledRejections).toHaveLength(0)
} finally {
process.off('unhandledRejection', onUnhandledRejection)
}
})
it('should warn in dev when onBroadcastError itself throws', async () => {
const key = queryKey()
process.env['NODE_ENV'] = 'development'
const cloneError = new DOMException('DataCloneError', 'DataCloneError')
const callbackError = new Error('boom')
mockPostMessage.mockRejectedValueOnce(cloneError)
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
try {
broadcastQueryClient({
queryClient,
broadcastChannel: 'test_channel',
onBroadcastError: () => {
throw callbackError
},
})
queryClient.setQueryData(key, { value: 1 })
await sleep(0)
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('onBroadcastError threw while handling'),
callbackError,
)
} finally {
warnSpy.mockRestore()
}
})
it('should not cause an unhandled rejection when async onBroadcastError rejects', async () => {
const key = queryKey()
const cloneError = new DOMException('DataCloneError', 'DataCloneError')
mockPostMessage.mockRejectedValueOnce(cloneError)
const unhandledRejections: Array<unknown> = []
const onUnhandledRejection = (reason: unknown) => {
unhandledRejections.push(reason)
}
process.on('unhandledRejection', onUnhandledRejection)
try {
const onBroadcastError = vi
.fn()
.mockRejectedValueOnce(new Error('async boom'))
broadcastQueryClient({
queryClient,
broadcastChannel: 'test_channel',
onBroadcastError,
})
queryClient.setQueryData(key, { value: 1 })
await sleep(10)
expect(onBroadcastError).toHaveBeenCalledWith(
cloneError,
expect.objectContaining<BroadcastErrorEvent>({
type: 'added',
queryHash: expect.any(String) as string,
queryKey: key,
}),
)
expect(unhandledRejections).toHaveLength(0)
} finally {
process.off('unhandledRejection', onUnhandledRejection)
}
})
it('should warn in dev when async onBroadcastError rejects', async () => {
const key = queryKey()
process.env['NODE_ENV'] = 'development'
const cloneError = new DOMException('DataCloneError', 'DataCloneError')
const asyncError = new Error('async boom')
mockPostMessage.mockRejectedValueOnce(cloneError)
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
try {
broadcastQueryClient({
queryClient,
broadcastChannel: 'test_channel',
onBroadcastError: () => Promise.reject(asyncError),
})
queryClient.setQueryData(key, { value: 1 })
await sleep(10)
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('onBroadcastError threw while handling'),
asyncError,
)
} finally {
warnSpy.mockRestore()
}
})
it('should call onBroadcastError when postMessage fails', async () => {
const key = queryKey()
const cloneError = new DOMException('DataCloneError', 'DataCloneError')
mockPostMessage.mockRejectedValueOnce(cloneError)
const onBroadcastError = vi.fn()
broadcastQueryClient({
queryClient,
broadcastChannel: 'test_channel',
onBroadcastError,
})
queryClient.setQueryData(key, { value: 1 })
await sleep(0)
expect(onBroadcastError).toHaveBeenCalledWith(
cloneError,
expect.objectContaining<BroadcastErrorEvent>({
type: 'added',
queryHash: expect.any(String) as string,
queryKey: key,
}),
)
})
it('should warn in dev when postMessage fails and onBroadcastError is not provided', async () => {
const key = queryKey()
process.env['NODE_ENV'] = 'development'
const cloneError = new DOMException('DataCloneError', 'DataCloneError')
mockPostMessage.mockRejectedValueOnce(cloneError)
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
try {
broadcastQueryClient({
queryClient,
broadcastChannel: 'test_channel',
})
queryClient.setQueryData(key, { value: 1 })
await sleep(0)
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('cross-tab sync for this query was skipped'),
cloneError,
)
} finally {
warnSpy.mockRestore()
}
})
it('should not warn in production when postMessage fails', async () => {
const key = queryKey()
process.env['NODE_ENV'] = 'production'
const cloneError = new DOMException('DataCloneError', 'DataCloneError')
mockPostMessage.mockRejectedValueOnce(cloneError)
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
try {
broadcastQueryClient({
queryClient,
broadcastChannel: 'test_channel',
})
queryClient.setQueryData(key, { value: 1 })
await sleep(0)
expect(warnSpy).not.toHaveBeenCalled()
} finally {
warnSpy.mockRestore()
}
})
})
})

View File

@@ -0,0 +1,196 @@
import { BroadcastChannel } from 'broadcast-channel'
import type { BroadcastChannelOptions } from 'broadcast-channel'
import type { QueryClient, QueryKey } from '@tanstack/query-core'
/**
* Metadata describing a broadcast that failed to be delivered to other tabs.
* Passed to {@link BroadcastQueryClientOptions.onBroadcastError} so callers
* can correlate failures with the originating query.
*/
export interface BroadcastErrorEvent {
type: 'updated' | 'removed' | 'added'
queryHash: string
queryKey: QueryKey
}
type BroadcastMessage =
| { type: 'updated'; queryHash: string; queryKey: QueryKey; state: unknown }
| { type: 'removed'; queryHash: string; queryKey: QueryKey }
| { type: 'added'; queryHash: string; queryKey: QueryKey; state: unknown }
interface BroadcastQueryClientOptions {
/** The QueryClient to sync. */
queryClient: QueryClient
/**
* Unique channel name used to communicate between tabs and windows.
* @default 'tanstack-query'
*/
broadcastChannel?: string
/** Options forwarded to the underlying `BroadcastChannel`. */
options?: BroadcastChannelOptions
/**
* Called when a query event fails to broadcast to other tabs — most
* commonly because the query's `state.data`, `state.error`, or `queryKey`
* contains a value the structured-clone algorithm cannot serialize
* (e.g. `ReadableStream`, `File`, functions, Vue `reactive` proxies).
*
* Provide this to route failures to an error tracker. If omitted, a
* `console.warn` is emitted in development so failures are never silent.
*
* May return a `Promise`; any rejection is caught internally so it cannot
* cause a secondary unhandled rejection.
*/
onBroadcastError?: (
error: unknown,
event: BroadcastErrorEvent,
) => void | Promise<void>
}
export function broadcastQueryClient({
queryClient,
broadcastChannel = 'tanstack-query',
options,
onBroadcastError,
}: BroadcastQueryClientOptions): () => void {
let transaction = false
const tx = (cb: () => void) => {
transaction = true
try {
cb()
} finally {
// Guard against `cb` throwing (e.g. `query.setState`/`queryCache.build`
// triggering a listener that throws while applying an incoming
// cross-tab message). Without this, `transaction` would stay `true`
// forever, silently disabling this tab's own broadcasts to other tabs
// for the rest of the session.
transaction = false
}
}
const channel = new BroadcastChannel(broadcastChannel, {
webWorkerSupport: false,
...options,
})
const queryCache = queryClient.getQueryCache()
const safePost = (message: BroadcastMessage): void => {
channel.postMessage(message).catch((error: unknown) => {
const event: BroadcastErrorEvent = {
type: message.type,
queryHash: message.queryHash,
queryKey: message.queryKey,
}
if (onBroadcastError) {
const warnCallbackError = (callbackError: unknown) => {
if (process.env.NODE_ENV !== 'production') {
console.warn(
`[broadcastQueryClient] onBroadcastError threw while handling "${event.type}" for query ${event.queryHash}.`,
callbackError,
)
}
}
let result: void | Promise<void>
try {
result = onBroadcastError(error, event)
} catch (callbackError) {
warnCallbackError(callbackError)
return
}
result?.catch(warnCallbackError)
} else if (process.env.NODE_ENV !== 'production') {
console.warn(
`[broadcastQueryClient] Failed to broadcast "${event.type}" event for query ${event.queryHash}. ` +
'The query value could not be structured-cloned; cross-tab sync for this query was skipped.',
error,
)
}
})
}
const unsubscribe = queryCache.subscribe((queryEvent) => {
if (transaction) {
return
}
const {
query: { queryHash, queryKey, state, observers },
} = queryEvent
if (queryEvent.type === 'updated' && queryEvent.action.type === 'success') {
safePost({
type: 'updated',
queryHash,
queryKey,
state,
})
}
if (queryEvent.type === 'removed' && observers.length > 0) {
safePost({
type: 'removed',
queryHash,
queryKey,
})
}
if (queryEvent.type === 'added') {
safePost({
type: 'added',
queryHash,
queryKey,
state,
})
}
})
channel.onmessage = (action) => {
if (!action?.type) {
return
}
tx(() => {
const { type, queryHash, queryKey, state } = action
const query = queryCache.get(queryHash)
if (type === 'updated') {
if (query) {
query.setState(state)
return
}
queryCache.build(
queryClient,
{
queryKey,
queryHash,
},
state,
)
} else if (type === 'removed') {
if (query) {
queryCache.remove(query)
}
} else if (type === 'added') {
if (query) {
query.setState(state)
return
}
queryCache.build(
queryClient,
{
queryKey,
queryHash,
},
state,
)
}
})
}
return () => {
unsubscribe()
channel.close()
}
}

View File

@@ -0,0 +1,14 @@
import '@testing-library/jest-dom/vitest'
import { act, cleanup as cleanupRTL } from '@testing-library/react'
import { afterEach } from 'vitest'
import { notifyManager } from '@tanstack/query-core'
// https://testing-library.com/docs/react-testing-library/api#cleanup
afterEach(() => {
cleanupRTL()
})
// Wrap notifications with act to make sure React knows about React Query updates
notifyManager.setNotifyFunction((fn) => {
act(fn)
})

View File

@@ -0,0 +1,15 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist-ts",
"rootDir": "."
},
"include": [
"src",
"test-setup.ts",
"*.config.ts",
"*.config.js",
"package.json"
],
"references": [{ "path": "../query-core" }]
}

View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"incremental": false,
"composite": false,
"rootDir": "../../"
},
"include": ["src"],
"exclude": ["src/__tests__"]
}

View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'tsdown'
import { legacyConfig, modernConfig } from './root.tsdown.config.js'
export default defineConfig([
modernConfig({ entry: ['src/*.ts'] }),
legacyConfig({ entry: ['src/*.ts'] }),
])

View File

@@ -0,0 +1,35 @@
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import packageJson from './package.json'
export default defineConfig({
plugins: [react()],
// fix from https://github.com/vitest-dev/vitest/issues/6992#issuecomment-2509408660
resolve: {
conditions: ['@tanstack/custom-condition'],
},
environments: {
ssr: {
resolve: {
conditions: ['@tanstack/custom-condition'],
},
},
},
test: {
name: packageJson.name,
dir: './src',
watch: false,
environment: 'jsdom',
setupFiles: ['test-setup.ts'],
coverage: {
enabled: !!process.env.CI,
provider: 'istanbul',
include: ['src/**/*'],
exclude: ['src/__tests__/**'],
},
typecheck: { enabled: true },
restoreMocks: true,
retry: process.env.CI ? 3 : 0,
},
})