Skip to content

Commit 6bd09aa

Browse files
authored
Merge pull request #1588 from abhishekg999/ahh/fix-utils-mergedeep-recursion
fix: add seen weakset during mergeDeep
2 parents 07b449a + 1819ba3 commit 6bd09aa

File tree

2 files changed

+82
-2
lines changed

2 files changed

+82
-2
lines changed

src/utils.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { isStringTextContainingNode } from 'typescript'
21
import type { Sucrose } from './sucrose'
32
import type { TraceHandler } from './trace'
43

@@ -59,14 +58,19 @@ export const mergeDeep = <
5958
skipKeys?: string[]
6059
override?: boolean
6160
mergeArray?: boolean
61+
seen?: WeakSet<object>
6262
}
6363
): A & B => {
6464
const skipKeys = options?.skipKeys
6565
const override = options?.override ?? true
6666
const mergeArray = options?.mergeArray ?? false
67+
const seen = options?.seen ?? new WeakSet<object>()
6768

6869
if (!isObject(target) || !isObject(source)) return target as A & B
6970

71+
if (seen.has(source)) return target as A & B
72+
seen.add(source)
73+
7074
for (const [key, value] of Object.entries(source)) {
7175
if (
7276
skipKeys?.includes(key) ||
@@ -98,11 +102,13 @@ export const mergeDeep = <
98102
target[key as keyof typeof target] = mergeDeep(
99103
(target as any)[key] as any,
100104
value,
101-
{ skipKeys, override, mergeArray }
105+
{ skipKeys, override, mergeArray, seen }
102106
)
103107
} catch {}
104108
}
105109

110+
seen.delete(source)
111+
106112
return target as A & B
107113
}
108114
export const mergeCookie = <const A extends Object, const B extends Object>(

test/units/merge-deep.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,78 @@ describe('mergeDeep', () => {
8686
.decorate('db', Object.freeze({ hello: 'world' }))
8787
.guard({}, (app) => app)
8888
})
89+
90+
it('handle circular references', () => {
91+
const a: {
92+
x: number
93+
toB?: typeof b
94+
} = { x: 1 }
95+
const b: {
96+
y: number
97+
toA?: typeof a
98+
} = { y: 2 }
99+
100+
a.toB = b
101+
b.toA = a
102+
103+
const target = {}
104+
const source = { prop: a }
105+
106+
const result = mergeDeep(target, source)
107+
108+
expect(result.prop.x).toBe(1)
109+
expect(result.prop.toB?.y).toBe(2)
110+
})
111+
112+
it('handle shared references in different branches', () => {
113+
const shared = { value: 123 }
114+
const target = { x: {}, y: {} }
115+
const source = { x: shared, y: shared }
116+
117+
const result = mergeDeep(target, source)
118+
119+
expect(result.x.value).toBe(123)
120+
expect(result.y.value).toBe(123)
121+
})
122+
123+
it('deduplicate plugin with circular decorators', async () => {
124+
const a: {
125+
x: number
126+
toB?: typeof b
127+
} = { x: 1 }
128+
const b: {
129+
y: number
130+
toA?: typeof a
131+
} = { y: 2 }
132+
a.toB = b
133+
b.toA = a
134+
135+
const complex = { a }
136+
137+
const Plugin = new Elysia({ name: 'Plugin', seed: 'seed' })
138+
.decorate('dep', complex)
139+
.as('scoped')
140+
141+
const ModuleA = new Elysia({ name: 'ModuleA' })
142+
.use(Plugin)
143+
.get('/moda/a', ({ dep }) => dep.a.x)
144+
.get('/moda/b', ({ dep }) => dep.a.toB?.y)
145+
146+
const ModuleB = new Elysia({ name: 'ModuleB' })
147+
.use(Plugin)
148+
.get('/modb/a', ({ dep }) => dep.a.x)
149+
.get('/modb/b', ({ dep }) => dep.a.toB?.y)
150+
151+
const app = new Elysia().use(ModuleA).use(ModuleB)
152+
153+
const resA = await app.handle(req('/moda/a')).then((x) => x.text())
154+
const resB = await app.handle(req('/modb/a')).then((x) => x.text())
155+
const resC = await app.handle(req('/moda/b')).then((x) => x.text())
156+
const resD = await app.handle(req('/modb/b')).then((x) => x.text())
157+
158+
expect(resA).toBe('1')
159+
expect(resB).toBe('1')
160+
expect(resC).toBe('2')
161+
expect(resD).toBe('2')
162+
})
89163
})

0 commit comments

Comments
 (0)