Skip to content

Commit fc68e41

Browse files
committed
add tests
1 parent e465cac commit fc68e41

File tree

2 files changed

+80
-4
lines changed

2 files changed

+80
-4
lines changed

src/utils.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,18 +58,18 @@ export const mergeDeep = <
5858
skipKeys?: string[]
5959
override?: boolean
6060
mergeArray?: boolean
61-
seen?: WeakSet<object>
61+
seen?: WeakMap<object, boolean>
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>()
67+
const seen = options?.seen ?? new WeakMap<object, boolean>()
6868

6969
if (!isObject(target) || !isObject(source)) return target as A & B
7070

71-
if (seen.has(source)) return target as A & B
72-
seen.add(source)
71+
if (seen.get(source)) return target as A & B
72+
seen.set(source, true)
7373

7474
for (const [key, value] of Object.entries(source)) {
7575
if (
@@ -107,6 +107,8 @@ export const mergeDeep = <
107107
} catch {}
108108
}
109109

110+
seen.delete(source)
111+
110112
return target as A & B
111113
}
112114
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)