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
50 changes: 50 additions & 0 deletions packages/eclipsa/core/reactive/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,54 @@ describe('signal', () => {
expect(cb).toHaveBeenNthCalledWith(2, '1:new')
expect(cb).toHaveBeenNthCalledWith(3, '2:new')
})

it('records read signals on the effect and unsubscribes them on dispose', () => {
const count = signal(0)
const cb = vi.fn()

const dispose = effect(() => {
cb(count.get())
})

expect(count.effects.size).toBe(1)
const [tracked] = count.effects
expect(tracked?.signals.has(count)).toBe(true)

dispose()

expect(count.effects.size).toBe(0)
count.set(1)
expect(cb).toHaveBeenCalledTimes(1)
})

it('restores the previous tracking context when an effect body throws', () => {
const count = signal(0)

expect(() =>
effect(() => {
throw new Error('boom')
}),
).toThrow('boom')

count.get()
expect(count.effects.size).toBe(0)
})

it('keeps tracking the outer effect after a nested effect finishes', () => {
const outer = signal(0)
const inner = signal(0)
const outerCb = vi.fn()

effect(() => {
effect(() => {
inner.get()
})
outerCb(outer.get())
})

expect(outerCb).toHaveBeenCalledTimes(1)
outer.set(1)
expect(outerCb).toHaveBeenCalledTimes(2)
expect(outerCb).toHaveBeenNthCalledWith(2, 1)
})
})
30 changes: 21 additions & 9 deletions packages/eclipsa/core/reactive/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const signal = <T>(init: T): Signal<T> => {
get() {
if (currentEffect) {
this.effects.add(currentEffect)
currentEffect.signals.add(this)
}
return value
},
Expand All @@ -45,23 +46,34 @@ export const signal = <T>(init: T): Signal<T> => {
return signal
}

const registry = new FinalizationRegistry<Effect>((effect) => {
const unsubscribeEffect = (effect: Effect) => {
for (const signal of effect.signals) {
signal.effects.delete(effect)
}
})
effect.signals.clear()
}

const registry = new FinalizationRegistry<Effect>(unsubscribeEffect)

export const effect = (fn: () => void) => {
export const effect = (fn: () => void): (() => void) => {
const newEffect: Effect = {
fn() {
fn()
},
fn,
signals: new Set(),
}

const previousEffect = currentEffect
currentEffect = newEffect
newEffect.fn()
currentEffect = null
try {
newEffect.fn()
} finally {
currentEffect = previousEffect
}

const dispose = () => {
registry.unregister(dispose)
unsubscribeEffect(newEffect)
}
registry.register(dispose, newEffect, dispose)

registry.register(fn, newEffect)
return dispose
}
Loading