Skip to content
Open
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
63 changes: 63 additions & 0 deletions packages/react-router-devtools/src/shared/bigint-util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,67 @@ describe("convertBigIntToString", () => {
const result = convertBigIntToString(123)
expect(result).toBe(123)
})

it("should replace circular references with [Circular]", () => {
const o: Record<string, unknown> = {}
o.self = o
const result = convertBigIntToString(o)
expect(result).toEqual({ self: "[Circular]" })
})

it("should replace circular reference at depth 1 with [Circular]", () => {
const root: Record<string, unknown> = { a: 1 }
root.nested = { b: 2, back: root }
const result = convertBigIntToString(root)
expect(result).toEqual({
a: 1,
nested: { b: 2, back: "[Circular]" },
})
})

it("should not replace shared (non-circular) references with [Circular]", () => {
const shared = { x: 1, y: 2 }
const root = { a: shared, b: shared }
const result = convertBigIntToString(root)
expect(result).toEqual({
a: { x: 1, y: 2 },
b: { x: 1, y: 2 },
})
})

it("should replace deep acyclic structure beyond maxDepth with [Max depth reached]", () => {
let deep: Record<string, unknown> = {}
const root = deep
for (let i = 0; i < 60; i++) {
deep.next = {}
deep = deep.next as Record<string, unknown>
}
const result = convertBigIntToString(root)
let current: unknown = result
let depth = 0
while (current !== null && typeof current === "object" && "next" in current) {
current = (current as Record<string, unknown>).next
depth++
}
expect(depth).toBe(50)
expect(current).toBe("[Max depth reached]")
})

it("should traverse deeper when maxDepth option is increased", () => {
let deep: Record<string, unknown> = {}
const root = deep
for (let i = 0; i < 60; i++) {
deep.next = {}
deep = deep.next as Record<string, unknown>
}
const result = convertBigIntToString(root, { maxDepth: 100 })
let current: unknown = result
let depth = 0
while (current !== null && typeof current === "object" && "next" in current) {
current = (current as Record<string, unknown>).next
depth++
}
expect(depth).toBe(60)
expect(current).toEqual({})
})
})
28 changes: 25 additions & 3 deletions packages/react-router-devtools/src/shared/bigint-util.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,41 @@
// biome-ignore lint/suspicious/noExplicitAny: we don't know the data
export const bigIntReplacer = (_key: any, value: any) => (typeof value === "bigint" ? value.toString() : value)

const DEFAULT_MAX_DEPTH = 50
const CIRCULAR_PLACEHOLDER = "[Circular]"
const MAX_DEPTH_PLACEHOLDER = "[Max depth reached]"

// biome-ignore lint/suspicious/noExplicitAny: we don't know the data
export const convertBigIntToString = (data: any): any => {
function convertBigIntToStringInner(data: any, depth: number, seen: WeakSet<object>, maxDepth: number): any {
if (typeof data === "bigint") {
return data.toString()
}

if (Array.isArray(data)) {
return data.map((item) => convertBigIntToString(item))
if (seen.has(data)) return CIRCULAR_PLACEHOLDER
if (depth >= maxDepth) return MAX_DEPTH_PLACEHOLDER
seen.add(data)
const result = data.map((item) => convertBigIntToStringInner(item, depth + 1, seen, maxDepth))
seen.delete(data)
return result
}

if (data !== null && typeof data === "object") {
return Object.fromEntries(Object.entries(data).map(([key, value]) => [key, convertBigIntToString(value)]))
if (seen.has(data)) return CIRCULAR_PLACEHOLDER
if (depth >= maxDepth) return MAX_DEPTH_PLACEHOLDER
seen.add(data)
const result = Object.fromEntries(
Object.entries(data).map(([key, value]) => [key, convertBigIntToStringInner(value, depth + 1, seen, maxDepth)]),
)
seen.delete(data)
return result
}

return data
}

// biome-ignore lint/suspicious/noExplicitAny: we don't know the data
export const convertBigIntToString = (data: any, options?: { maxDepth?: number }): any => {
const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH
return convertBigIntToStringInner(data, 0, new WeakSet(), maxDepth)
}