diff --git a/packages/agera/src/effect.spec.ts b/packages/agera/src/effect.spec.ts index e7a80565..782f8d93 100644 --- a/packages/agera/src/effect.spec.ts +++ b/packages/agera/src/effect.spec.ts @@ -16,6 +16,7 @@ import { trigger, mountable, onMounted, + createScope, deferScope, boundDeferScope, startScope, @@ -2207,6 +2208,35 @@ describe('agera', () => { }) }) + describe('createScope', () => { + it('should stop a scope created under it whose body threw', () => { + const $count = signal(1) + const scope = createScope() + + expect(() => deferScope(() => { + computed(() => $count() * 2)() + throw new Error('Failed') + }, scope)).toThrow('Failed') + expect($count.node.subs).toBeDefined() + + stopScope(scope) + + expect($count.node.subs).toBeUndefined() + }) + + it('should be stopped with its parent', () => { + const $count = signal(1) + const parent = createScope() + + deferScope(() => { + computed(() => $count() * 2)() + }, createScope(parent)) + stopScope(parent) + + expect($count.node.subs).toBeUndefined() + }) + }) + describe('observe', () => { it('should not trigger signal mount', () => { const $num = mountable(signal(0)) diff --git a/packages/agera/src/effect.ts b/packages/agera/src/effect.ts index 5b53ff38..3295b21a 100644 --- a/packages/agera/src/effect.ts +++ b/packages/agera/src/effect.ts @@ -7,6 +7,7 @@ import { effect, deferEffect, effectScope, + createScope, deferScope, boundDeferScope, startScope, @@ -22,6 +23,7 @@ export { deferEffect, boundDeferScope, effectScope, + createScope, deferScope, startScope, stopScope, diff --git a/packages/agera/src/internals/system.ts b/packages/agera/src/internals/system.ts index 5cd8d67c..fa5481cb 100644 --- a/packages/agera/src/internals/system.ts +++ b/packages/agera/src/internals/system.ts @@ -1304,7 +1304,14 @@ export function selector( // state of that site, so they follow the parent's start and stop. // -function createScope(parent: ReactiveNode | undefined): ReactiveNode { +/** + * Create an empty scope to create scopes under. Stopping it stops all of + * them, even one whose body threw before `deferScope` returned its handle. + * @internal + * @param parent - Optional parent scope to link the created scope to. + * @returns Scope handle. + */ +export function createScope(parent?: DeferredScope): DeferredScope { const e: ReactiveNode = { deps: undefined, depsTail: undefined, @@ -1319,24 +1326,24 @@ function createScope(parent: ReactiveNode | undefined): ReactiveNode { } if (parent !== undefined) { - link(e, parent, 0) + link(e, parent as unknown as ReactiveNode, 0) // Inherit the state of the position, exactly as `effectScope` does, so // a subtree defers and starts as one - e.modes |= parent.modes & LazyMode + e.modes |= (parent as unknown as ReactiveNode).modes & LazyMode } - return e + return e as unknown as DeferredScope } /** * Defer scope creation to delay its effects execution. * @internal * @param fn - The deferred scope function to run. - * @param parent - Optional parent scope node to link the created scope to. + * @param parent - Optional parent scope to link the created scope to. * @returns Deferred scope handle. */ -export function deferScope(fn: () => void, parent?: ReactiveNode): DeferredScope { - const e = createScope(parent) +export function deferScope(fn: () => void, parent?: DeferredScope): DeferredScope { + const e = createScope(parent) as unknown as ReactiveNode const prevSub = pushActiveSub(e) // The single mint of the token: a deferred scope is lazy wherever it is @@ -1595,7 +1602,7 @@ export function resumeScope(scope: DeferredScope): void { * @returns Function to create linked deferred scopes. */ export function boundDeferScope() { - const anchor = createScope(activeSub) + const anchor = createScope(activeSub as unknown as DeferredScope | undefined) return (fn: () => void, replace?: DeferredScope): DeferredScope => { if (replace !== undefined) { diff --git a/packages/store/.size-limit.js b/packages/store/.size-limit.js index 49fa7218..da2c82c5 100644 --- a/packages/store/.size-limit.js +++ b/packages/store/.size-limit.js @@ -22,7 +22,7 @@ export default [ name: 'All publics (Brotli)', path: 'dist/index.production.js', import: '*', - limit: '6.3 kB', + limit: '6.35 kB', modifyRolldownConfig }, {