-
Notifications
You must be signed in to change notification settings - Fork 4
fix: safely serialize query/mutation data to prevent DataCloneError with framework proxies #182
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -19,6 +19,20 @@ import { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { TanStackQueryActionExecutor } from "./modules/action-executor"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { sendToContentScript } from "./modules/message-sender"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Safely deep-clone a value into a plain, structured-cloneable object. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Frameworks like Vue 3, MobX, and Solid wrap state in Proxy objects that | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * cannot be passed through `window.postMessage` (structured clone algorithm). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * JSON round-tripping reads through Proxy getters, producing plain objects. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function safeClone<T>(value: T): T { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return JSON.parse(JSON.stringify(value)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+26
to
+30
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * JSON round-tripping reads through Proxy getters, producing plain objects. | |
| */ | |
| function safeClone<T>(value: T): T { | |
| try { | |
| return JSON.parse(JSON.stringify(value)); | |
| * We first prefer `structuredClone` to preserve types, and fall back to | |
| * JSON round-tripping with a custom replacer/reviver to handle common | |
| * built-in types (Date, Set, Map, RegExp, BigInt, NaN, Infinity). | |
| */ | |
| const globalStructuredClone: (<T>(value: T) => T) | undefined = | |
| typeof (globalThis as any).structuredClone === "function" | |
| ? ((globalThis as any).structuredClone as <T>(value: T) => T) | |
| : undefined; | |
| function devtoolsJsonReplacer(_key: string, value: unknown): unknown { | |
| // Preserve BigInt by converting to a tagged string representation. | |
| if (typeof value === "bigint") { | |
| return { __devtools_type: "BigInt", value: value.toString() }; | |
| } | |
| // Preserve Dates. | |
| if (value instanceof Date) { | |
| return { __devtools_type: "Date", value: value.toISOString() }; | |
| } | |
| // Preserve Sets as arrays. | |
| if (value instanceof Set) { | |
| return { | |
| __devtools_type: "Set", | |
| value: Array.from(value.values()), | |
| }; | |
| } | |
| // Preserve Maps as [key, value] entry arrays. | |
| if (value instanceof Map) { | |
| return { | |
| __devtools_type: "Map", | |
| value: Array.from(value.entries()), | |
| }; | |
| } | |
| // Preserve regular expressions. | |
| if (value instanceof RegExp) { | |
| return { | |
| __devtools_type: "RegExp", | |
| value: value.source, | |
| flags: value.flags, | |
| }; | |
| } | |
| // Preserve special number values that JSON would otherwise turn into null. | |
| if (typeof value === "number" && !Number.isFinite(value)) { | |
| return { __devtools_type: "Number", value: value.toString() }; | |
| } | |
| return value; | |
| } | |
| function devtoolsJsonReviver(_key: string, value: unknown): unknown { | |
| if ( | |
| value && | |
| typeof value === "object" && | |
| Object.prototype.hasOwnProperty.call(value, "__devtools_type") | |
| ) { | |
| const tagged = value as { | |
| __devtools_type: string; | |
| value?: unknown; | |
| flags?: string; | |
| }; | |
| switch (tagged.__devtools_type) { | |
| case "BigInt": | |
| return typeof tagged.value === "string" | |
| ? BigInt(tagged.value) | |
| : value; | |
| case "Date": | |
| return typeof tagged.value === "string" | |
| ? new Date(tagged.value) | |
| : value; | |
| case "Set": | |
| return Array.isArray(tagged.value) | |
| ? new Set(tagged.value) | |
| : value; | |
| case "Map": | |
| return Array.isArray(tagged.value) | |
| ? new Map(tagged.value as [any, any][]) | |
| : value; | |
| case "RegExp": | |
| return typeof tagged.value === "string" | |
| ? new RegExp(tagged.value, tagged.flags || "") | |
| : value; | |
| case "Number": | |
| if (tagged.value === "NaN") return NaN; | |
| if (tagged.value === "Infinity") return Infinity; | |
| if (tagged.value === "-Infinity") return -Infinity; | |
| return value; | |
| default: | |
| return value; | |
| } | |
| } | |
| return value; | |
| } | |
| function safeClone<T>(value: T): T { | |
| // Prefer native structuredClone when available to preserve types where possible. | |
| if (globalStructuredClone) { | |
| try { | |
| return globalStructuredClone(value); | |
| } catch { | |
| // Fall through to JSON-based cloning if structuredClone fails | |
| // (for example, on certain Proxy-wrapped values). | |
| } | |
| } | |
| try { | |
| const json = JSON.stringify(value, devtoolsJsonReplacer); | |
| return JSON.parse(json, devtoolsJsonReviver); |
Copilot
AI
Feb 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The silent error handling (empty catch block) makes debugging difficult. If safeClone fails, developers won't know which field caused the serialization error or why. This is especially problematic since the function is called on multiple different fields (queryKey, data, error, variables, context, meta).
Consider logging a warning when serialization fails to aid debugging. For example: console.warn('Failed to serialize value:', error); This follows the pattern used elsewhere in the codebase for non-critical errors.
| } catch { | |
| } catch (error) { | |
| console.warn("Failed to serialize value in safeClone:", error); |
Copilot
AI
Feb 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The return type "[Unserializable]" as T creates a type safety issue. When T is expected to be an object or array (like queryKey, which is QueryKey type), returning a string breaks the type contract. This could cause runtime errors in code that expects the original type structure.
Consider either:
- Returning
{} as Tfor objects/arrays to maintain structural compatibility - Adding proper type narrowing to handle different value types appropriately
- Using a union type that allows the function to return the original type or a serialization error indicator
| function safeClone<T>(value: T): T { | |
| try { | |
| return JSON.parse(JSON.stringify(value)); | |
| } catch { | |
| return "[Unserializable]" as T; | |
| function safeClone<T>(value: T): T | "[Unserializable]" { | |
| try { | |
| return JSON.parse(JSON.stringify(value)); | |
| } catch { | |
| return "[Unserializable]"; |
Copilot
AI
Feb 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The expression query.meta || {} already creates a new plain object when meta is falsy, so safeClone is unnecessary in that case. Consider optimizing to: query.meta ? safeClone(query.meta) : {} to avoid the overhead of JSON serialization when meta is undefined or null.
| meta: safeClone(query.meta || {}), | |
| meta: query.meta ? safeClone(query.meta) : {}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JSON.parse(JSON.stringify()) is called on every query/mutation update, potentially multiple times per second during active operations. This creates performance overhead since:
Consider optimizing by:
The performance impact will be especially noticeable with large query caches or complex data objects.