Skip to content

Commit ce512cc

Browse files
committed
feat: implement cleanup on destroy for async computed
1 parent cecfdc5 commit ce512cc

File tree

7 files changed

+98
-46
lines changed

7 files changed

+98
-46
lines changed

packages/qwik/src/core/client/vnode-diff.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ import { hasClassAttr } from '../shared/utils/scoped-styles';
5050
import { serializeAttribute } from '../shared/utils/styles';
5151
import { isArray, type ValueOrPromise } from '../shared/utils/types';
5252
import { trackSignalAndAssignHost } from '../use/use-core';
53-
import { TaskFlags, cleanupTask, isTask } from '../use/use-task';
53+
import { TaskFlags, isTask } from '../use/use-task';
5454
import type { DomContainer } from './dom-container';
5555
import { VNodeFlags, type ClientAttrs, type ClientContainer } from './types';
5656
import { mapApp_findIndx, mapArray_set } from './util-mapArray';
@@ -81,6 +81,9 @@ import {
8181
} from './vnode';
8282
import type { ElementVNode, TextVNode, VNode, VirtualVNode } from './vnode-impl';
8383
import { getAttributeNamespace, getNewElementNamespaceData } from './vnode-namespace';
84+
import { cleanupDestroyable, isDestroyable } from '../use/utils/destroyable';
85+
import { SignalImpl } from '../reactive-primitives/impl/signal-impl';
86+
import { isStore } from '../reactive-primitives/impl/store';
8487

8588
export const vnode_diff = (
8689
container: ClientContainer,
@@ -1527,29 +1530,33 @@ export function cleanup(container: ClientContainer, vNode: VNode) {
15271530
if (type & VNodeFlags.ELEMENT_OR_VIRTUAL_MASK) {
15281531
clearAllEffects(container, vCursor);
15291532
markVNodeAsDeleted(vCursor);
1530-
// Only elements and virtual nodes need to be traversed for children
1531-
if (type & VNodeFlags.Virtual) {
1533+
1534+
const isComponent =
1535+
type & VNodeFlags.Virtual &&
1536+
(vCursor as VirtualVNode).getProp<OnRenderFn<any> | null>(OnRenderProp, null) !== null;
1537+
if (isComponent) {
1538+
// cleanup q:seq content
15321539
const seq = container.getHostProp<Array<any>>(vCursor as VirtualVNode, ELEMENT_SEQ);
15331540
if (seq) {
15341541
for (let i = 0; i < seq.length; i++) {
15351542
const obj = seq[i];
15361543
if (isTask(obj)) {
1537-
const task = obj;
1538-
clearAllEffects(container, task);
1539-
if (task.$flags$ & TaskFlags.VISIBLE_TASK) {
1540-
container.$scheduler$(ChoreType.CLEANUP_VISIBLE, task);
1541-
} else {
1542-
cleanupTask(task);
1544+
clearAllEffects(container, obj);
1545+
if (obj.$flags$ & TaskFlags.VISIBLE_TASK) {
1546+
container.$scheduler$(ChoreType.CLEANUP_VISIBLE, obj);
1547+
// don't call cleanupDestroyable yet, do it by the scheduler
1548+
continue;
15431549
}
1550+
} else if (obj instanceof SignalImpl || isStore(obj)) {
1551+
clearAllEffects(container, obj);
1552+
}
1553+
1554+
if (isDestroyable(obj)) {
1555+
cleanupDestroyable(obj);
15441556
}
15451557
}
15461558
}
1547-
}
15481559

1549-
const isComponent =
1550-
type & VNodeFlags.Virtual &&
1551-
(vCursor as VirtualVNode).getProp<OnRenderFn<any> | null>(OnRenderProp, null) !== null;
1552-
if (isComponent) {
15531560
// SPECIAL CASE: If we are a component, we need to descend into the projected content and release the content.
15541561
const attrs = vnode_getProps(vCursor as VirtualVNode);
15551562
for (let i = 0; i < attrs.length; i = i + 2) {

packages/qwik/src/core/shared/scheduler.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,7 @@ import {
9898
import { scheduleEffects } from '../reactive-primitives/utils';
9999
import { type ISsrNode, type SSRContainer } from '../ssr/ssr-types';
100100
import { runResource, type ResourceDescriptor } from '../use/use-resource';
101-
import {
102-
Task,
103-
TaskFlags,
104-
cleanupTask,
105-
runTask,
106-
type DescriptorBase,
107-
type TaskFn,
108-
} from '../use/use-task';
101+
import { Task, TaskFlags, runTask, type DescriptorBase, type TaskFn } from '../use/use-task';
109102
import { executeComponent } from './component-execution';
110103
import type { OnRenderFn } from './component.public';
111104
import type { Props } from './jsx/jsx-runtime';
@@ -127,6 +120,7 @@ import { isSsrNode } from '../reactive-primitives/subscriber';
127120
import { logWarn } from './utils/log';
128121
import type { ElementVNode, VirtualVNode } from '../client/vnode-impl';
129122
import { ChoreArray, choreComparator } from '../client/chore-array';
123+
import { cleanupDestroyable } from '../use/utils/destroyable';
130124

131125
// Turn this on to get debug output of what the scheduler is doing.
132126
const DEBUG: boolean = false;
@@ -700,7 +694,7 @@ This is often caused by modifying a signal in an already rendered component duri
700694
case ChoreType.CLEANUP_VISIBLE:
701695
{
702696
const task = chore.$payload$ as Task<TaskFn, TaskFn>;
703-
cleanupTask(task);
697+
cleanupDestroyable(task);
704698
}
705699
break;
706700
case ChoreType.NODE_DIFF:

packages/qwik/src/core/tests/use-async-computed.spec.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,4 +307,45 @@ describe.each([
307307
(globalThis as any).log = undefined;
308308
});
309309
});
310+
311+
describe('cleanup', () => {
312+
it('should run cleanup on destroy', async () => {
313+
(globalThis as any).log = [];
314+
315+
const Child = component$(() => {
316+
const asyncValue = useAsyncComputed$(({ cleanup }) => {
317+
cleanup(() => {
318+
(globalThis as any).log.push('cleanup');
319+
});
320+
return Promise.resolve(1);
321+
});
322+
return <div>{asyncValue.value}</div>;
323+
});
324+
325+
const Counter = component$(() => {
326+
const toggle = useSignal(true);
327+
328+
return (
329+
<>
330+
<button onClick$={() => (toggle.value = !toggle.value)}></button>
331+
{toggle.value && <Child />}
332+
</>
333+
);
334+
});
335+
const { container } = await render(<Counter />, { debug });
336+
// on server its called after render
337+
// on client it is not called yet
338+
expect((globalThis as any).log).toEqual(render === ssrRenderToDom ? ['cleanup'] : []);
339+
await trigger(container.element, 'button', 'click');
340+
// on server after resuming cleanup is not called yet
341+
// on client it is called as usual
342+
expect((globalThis as any).log).toEqual(
343+
render === ssrRenderToDom ? ['cleanup'] : ['cleanup']
344+
);
345+
await trigger(container.element, 'button', 'click'); //show
346+
await trigger(container.element, 'button', 'click'); //hide
347+
// on server and client cleanup called again
348+
expect((globalThis as any).log).toEqual(['cleanup', 'cleanup']);
349+
});
350+
});
310351
});

packages/qwik/src/core/use/use-resource.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { isServerPlatform } from '../shared/platform/platform';
44
import { assertQrl } from '../shared/qrl/qrl-utils';
55
import { type QRL } from '../shared/qrl/qrl.public';
66
import { invoke, newInvokeContext, untrack, useBindInvokeContext } from './use-core';
7-
import { Task, TaskFlags, cleanupTask, type DescriptorBase, type Tracker } from './use-task';
7+
import { Task, TaskFlags, type DescriptorBase, type Tracker } from './use-task';
88

99
import type { Container, HostElement, ValueOrPromise } from '../../server/qwik-types';
1010
import { clearAllEffects } from '../reactive-primitives/cleanup';
@@ -24,6 +24,7 @@ import { delay, isPromise, retryOnPromise, safeCall } from '../shared/utils/prom
2424
import { isObject } from '../shared/utils/types';
2525
import { useSequentialScope } from './use-sequential-scope';
2626
import { cleanupFn, trackFn } from './utils/tracker';
27+
import { cleanupDestroyable } from './utils/destroyable';
2728

2829
const DEBUG: boolean = false;
2930

@@ -265,7 +266,7 @@ export const runResource = <T>(
265266
host: HostElement
266267
): ValueOrPromise<void> => {
267268
task.$flags$ &= ~TaskFlags.DIRTY;
268-
cleanupTask(task);
269+
cleanupDestroyable(task);
269270

270271
const iCtx = newInvokeContext(container.$locale$, host, undefined, ResourceEvent);
271272
iCtx.$container$ = container;
@@ -373,7 +374,7 @@ export const runResource = <T>(
373374
promise,
374375
delay(timeout).then(() => {
375376
if (setState(false, new Error('timeout'))) {
376-
cleanupTask(task);
377+
cleanupDestroyable(task);
377378
}
378379
}),
379380
]);

packages/qwik/src/core/use/use-task.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { assertQrl } from '../shared/qrl/qrl-utils';
66
import type { QRL } from '../shared/qrl/qrl.public';
77
import { type Container, type HostElement } from '../shared/types';
88
import { ChoreType } from '../shared/util-chore-type';
9-
import { logError } from '../shared/utils/log';
109
import { TaskEvent } from '../shared/utils/markers';
1110
import { isPromise, safeCall } from '../shared/utils/promises';
1211
import { type NoSerialize } from '../shared/serdes/verify';
@@ -16,6 +15,7 @@ import { useLexicalScope } from './use-lexical-scope.public';
1615
import type { ResourceReturnInternal } from './use-resource';
1716
import { useSequentialScope } from './use-sequential-scope';
1817
import { cleanupFn, trackFn } from './utils/tracker';
18+
import { cleanupDestroyable } from './utils/destroyable';
1919

2020
export const enum TaskFlags {
2121
VISIBLE_TASK = 1 << 0,
@@ -168,7 +168,7 @@ export const runTask = (
168168
host: HostElement
169169
): ValueOrPromise<void> => {
170170
task.$flags$ &= ~TaskFlags.DIRTY;
171-
cleanupTask(task);
171+
cleanupDestroyable(task);
172172
const iCtx = newInvokeContext(container.$locale$, host, undefined, TaskEvent);
173173
iCtx.$container$ = container;
174174
const taskFn = task.$qrl$.getFn(iCtx, () => clearAllEffects(container, task)) as TaskFn;
@@ -191,18 +191,6 @@ export const runTask = (
191191
);
192192
};
193193

194-
export const cleanupTask = (task: Task) => {
195-
const destroy = task.$destroy$;
196-
if (destroy) {
197-
task.$destroy$ = null;
198-
try {
199-
destroy();
200-
} catch (err) {
201-
logError(err);
202-
}
203-
}
204-
};
205-
206194
export class Task<T = unknown, B = T>
207195
extends BackRef
208196
implements DescriptorBase<unknown, Signal<B> | ResourceReturnInternal<B>>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { AsyncComputedSignalImpl } from '../../reactive-primitives/impl/async-computed-signal-impl';
2+
import type { NoSerialize } from '../../shared/serdes/verify';
3+
import { logError } from '../../shared/utils/log';
4+
import { isTask } from '../use-task';
5+
6+
export type Destroyable = { $destroy$: NoSerialize<() => void> | null };
7+
8+
export const cleanupDestroyable = (destroyable: Destroyable) => {
9+
const destroy = destroyable.$destroy$;
10+
if (destroy) {
11+
destroyable.$destroy$ = null;
12+
try {
13+
destroy();
14+
} catch (err) {
15+
logError(err);
16+
}
17+
}
18+
};
19+
20+
export const isDestroyable = (obj: unknown): obj is Destroyable => {
21+
return isTask(obj) || obj instanceof AsyncComputedSignalImpl;
22+
};

packages/qwik/src/core/use/utils/tracker.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,12 @@ import { getSubscriber } from '../../reactive-primitives/subscriber';
99
import { EffectProperty, STORE_ALL_PROPS, type Consumer } from '../../reactive-primitives/types';
1010
import { isSignal } from '../../reactive-primitives/utils';
1111
import { qError, QError } from '../../shared/error/error';
12+
import { noSerialize } from '../../shared/serdes/verify';
1213
import type { Container } from '../../shared/types';
13-
import { noSerialize, type NoSerialize } from '../../shared/serdes/verify';
1414
import { isFunction, isObject } from '../../shared/utils/types';
1515
import { invoke, newInvokeContext } from '../use-core';
16-
import type { Tracker } from '../use-task';
17-
18-
export type Destroyable = { $destroy$: NoSerialize<() => void> | null };
16+
import { type Tracker } from '../use-task';
17+
import type { Destroyable } from './destroyable';
1918

2019
export const trackFn =
2120
(target: Consumer, container: Container | null): Tracker =>
@@ -57,13 +56,13 @@ export const cleanupFn = <T extends Destroyable>(
5756
cleanupFns = [];
5857
target.$destroy$ = noSerialize(() => {
5958
target.$destroy$ = null;
60-
cleanupFns!.forEach((fn) => {
59+
for (const fn of cleanupFns!) {
6160
try {
6261
fn();
6362
} catch (err) {
6463
handleError(err);
6564
}
66-
});
65+
}
6766
});
6867
}
6968
cleanupFns.push(fn);

0 commit comments

Comments
 (0)