Skip to content

Commit 3b62ddd

Browse files
committed
feat(agera): derive mounted state from the graph instead of counting subscribers
- replace the subscriber counters with a presence model: mounted state is never stored, it is a question asked of the graph - does this node have a live path up to an effect - memoized per drain and invalidated by any edge edit - deliver mount and unmount as one level change from a coalescing queue at quiescent boundaries: churn inside a turn is silent, sources mount before their dependents and unmount after them - move the layer behind two lazy sockets wired on first `mountable()` use, so a signal-only bundle drops it entirely: `signal` -17%, `nanoviews` -4% - `onMounted` and the new `isMounted` live in `signal.ts` over a plain listener list; `node.subsCount` and `node.mounted` are gone - an `onMounted` listener registered while the signal is already mounted now gets `true` at the next boundary instead of waiting for the next cycle - fix late contagion and late `mountable()` never mounting a signal, a cold computed unmounting a signal under a live subscriber, stuck `mounted(true)` on discarded subscriptions, listeners skipped or served twice when one un-subscribes or registers during a fire, and delivery lost for other signals when a listener throws
1 parent dd3169f commit 3b62ddd

19 files changed

Lines changed: 763 additions & 297 deletions

packages/agera/.size-limit.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@
33
"name": "All publics",
44
"path": "dist/index.js",
55
"import": "*",
6-
"limit": "2.63 kB"
6+
"limit": "2.72 kB"
77
},
88
{
99
"name": "Signal",
1010
"path": "dist/index.js",
1111
"import": "{ signal }",
12-
"limit": "1.61 kB"
12+
"limit": "1.34 kB"
1313
},
1414
{
1515
"name": "Minimal set",
1616
"path": "dist/index.js",
1717
"import": "{ signal, computed, effect }",
18-
"limit": "1.8 kB"
18+
"limit": "1.51 kB"
1919
},
2020
{
2121
"name": "Popular set",

packages/agera/oxlint.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default defineConfig({
1717
rules: {
1818
'typescript/no-invalid-void-type': 'off',
1919
'eslint/no-return-assign': 'off',
20-
'eslint/no-multi-assign': 'off'
20+
'eslint/no-multi-assign': 'off',
21+
'eslint/no-param-reassign': 'off'
2122
}
2223
})

packages/agera/src/effect.spec.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import {
1919
boundDeferScope,
2020
startScope,
2121
stopScope,
22-
observe
22+
observe,
23+
isMounted
2324
} from './index.js'
2425

2526
describe('agera', () => {
@@ -1325,7 +1326,7 @@ describe('agera', () => {
13251326
dispose()
13261327
})
13271328

1328-
it('should not desync mountable subs count on direct signal read in scope', () => {
1329+
it('should not change mounted state on direct signal read in scope', () => {
13291330
const $a = mountable(signal(1))
13301331
const log: boolean[] = []
13311332

@@ -1344,21 +1345,21 @@ describe('agera', () => {
13441345
})
13451346
})
13461347

1347-
expect($a.node.subsCount).toBe(2)
1348+
expect(isMounted($a)).toBe(true)
13481349
expect(log).toEqual([true])
13491350

13501351
stop()
13511352

1352-
expect($a.node.subsCount).toBe(1)
1353+
expect(isMounted($a)).toBe(true)
13531354
expect(log).toEqual([true])
13541355

13551356
stopOuter()
13561357

1357-
expect($a.node.subsCount).toBe(0)
1358+
expect(isMounted($a)).toBe(false)
13581359
expect(log).toEqual([true, false])
13591360
})
13601361

1361-
it('should decrement effect count on stop', () => {
1362+
it('should unmount dependencies on scope stop', () => {
13621363
const $a = mountable(signal(1))
13631364
const $b = mountable(signal(1))
13641365
const aListener = vi.fn()
@@ -1380,13 +1381,13 @@ describe('agera', () => {
13801381
})
13811382
})
13821383

1383-
expect($a.node.subsCount).toBe(2)
1384-
expect($b.node.subsCount).toBe(1)
1384+
expect(isMounted($a)).toBe(true)
1385+
expect(isMounted($b)).toBe(true)
13851386

13861387
stop()
13871388

1388-
expect($a.node.subsCount).toBe(0)
1389-
expect($b.node.subsCount).toBe(0)
1389+
expect(isMounted($a)).toBe(false)
1390+
expect(isMounted($b)).toBe(false)
13901391
})
13911392
})
13921393

@@ -1924,7 +1925,7 @@ describe('agera', () => {
19241925
startScope(deferScope(() => { /* flush mounted queue */ }))
19251926

19261927
expect(callback).not.toHaveBeenCalled()
1927-
expect($num.node.subsCount).toBe(0)
1928+
expect(isMounted($num)).toBe(false)
19281929
})
19291930

19301931
it('should not crash batch flush after scope stop empties its parent', () => {
@@ -1976,7 +1977,7 @@ describe('agera', () => {
19761977

19771978
expect(cleanupEvents).toEqual(['effect cleanup'])
19781979
expect(mountedEvents).toEqual([])
1979-
expect($source.node.subsCount).toBe(0)
1980+
expect(isMounted($source)).toBe(false)
19801981
})
19811982

19821983
it('should not start linked scope under stopped parent', () => {
@@ -2210,12 +2211,12 @@ describe('agera', () => {
22102211
const observeCallback = vi.fn()
22112212
const stop = observe($num, observeCallback)
22122213

2213-
expect($num.node.subsCount).toBe(0)
2214+
expect(isMounted($num)).toBe(false)
22142215

22152216
$num(1)
22162217

22172218
expect(observeCallback).toHaveBeenCalledWith(1)
2218-
expect($num.node.subsCount).toBe(0)
2219+
expect(isMounted($num)).toBe(false)
22192220

22202221
stop()
22212222
})

packages/agera/src/effect.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import {
1010
boundDeferScope,
1111
startScope,
1212
stopScope,
13-
untracked
13+
untracked,
14+
noMount
1415
} from './internals/system.js'
15-
import { noMount } from './internals/lifecycle.js'
1616

1717
export {
1818
effect,

packages/agera/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ export {
55
trigger,
66
onSignal
77
} from './internals/system.js'
8-
export { noMount } from './internals/lifecycle.js'
98
export * from './signal.js'
109
export * from './modes.js'
1110
export * from './effect.js'

packages/agera/src/internals/lifecycle.ts

Lines changed: 0 additions & 149 deletions
This file was deleted.

packages/agera/src/internals/oxlint.config.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,9 @@ export default defineConfig({
1212
'typescript/no-unsafe-assignment': 'off',
1313
'typescript/prefer-for-of': 'off',
1414
'typescript/prefer-optional-chain': 'off',
15-
'eslint/no-param-reassign': 'off',
1615
'eslint/no-label-var': 'off',
1716
'eslint/no-labels': 'off',
1817
'eslint/no-constant-condition': 'off',
19-
'eslint/no-multi-assign': 'off',
2018
'eslint/no-sequences': 'off'
2119
}
2220
})

0 commit comments

Comments
 (0)