Skip to content

Commit 9d1612d

Browse files
committed
feat: implement useAsyncComputed$
1 parent a6efe7c commit 9d1612d

File tree

13 files changed

+371
-7
lines changed

13 files changed

+371
-7
lines changed

packages/docs/src/routes/api/qwik/api.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,34 @@
220220
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/qrl/qrl.public.ts",
221221
"mdFile": "core._.md"
222222
},
223+
{
224+
"name": "AsyncComputedFn",
225+
"id": "asynccomputedfn",
226+
"hierarchy": [
227+
{
228+
"name": "AsyncComputedFn",
229+
"id": "asynccomputedfn"
230+
}
231+
],
232+
"kind": "TypeAlias",
233+
"content": "```typescript\nexport type AsyncComputedFn<T> = (ctx: TaskCtx) => Promise<T>;\n```\n**References:** [TaskCtx](#taskctx)",
234+
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-async-computed.ts",
235+
"mdFile": "core.asynccomputedfn.md"
236+
},
237+
{
238+
"name": "AsyncComputedReturnType",
239+
"id": "asynccomputedreturntype",
240+
"hierarchy": [
241+
{
242+
"name": "AsyncComputedReturnType",
243+
"id": "asynccomputedreturntype"
244+
}
245+
],
246+
"kind": "TypeAlias",
247+
"content": "```typescript\nexport type AsyncComputedReturnType<T> = T extends Promise<infer T> ? ReadonlySignal<T> : ReadonlySignal<T>;\n```\n**References:** [ReadonlySignal](#readonlysignal)",
248+
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-async-computed.ts",
249+
"mdFile": "core.asynccomputedreturntype.md"
250+
},
223251
{
224252
"name": "cache",
225253
"id": "resourcectx-cache",
@@ -2134,6 +2162,20 @@
21342162
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/reactive-primitives/impl/store.ts",
21352163
"mdFile": "core.unwrapstore.md"
21362164
},
2165+
{
2166+
"name": "useAsyncComputed$",
2167+
"id": "useasynccomputed_",
2168+
"hierarchy": [
2169+
{
2170+
"name": "useAsyncComputed$",
2171+
"id": "useasynccomputed_"
2172+
}
2173+
],
2174+
"kind": "Function",
2175+
"content": "Creates a computed signal which is calculated from the given function. A computed signal is a signal which is calculated from other signals. When the signals change, the computed signal is recalculated, and if the result changed, all tasks which are tracking the signal will be re-run and all components that read the signal will be re-rendered.\n\nThe function must be synchronous and must not have any side effects.\n\n\n```typescript\nuseAsyncComputed$: <T>(qrl: AsyncComputedFn<T>) => AsyncComputedReturnType<T>\n```\n\n\n<table><thead><tr><th>\n\nParameter\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nqrl\n\n\n</td><td>\n\n[AsyncComputedFn](#asynccomputedfn)<!-- -->&lt;T&gt;\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\n[AsyncComputedReturnType](#asynccomputedreturntype)<!-- -->&lt;T&gt;",
2176+
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-async-computed.ts",
2177+
"mdFile": "core.useasynccomputed_.md"
2178+
},
21372179
{
21382180
"name": "useComputed$",
21392181
"id": "usecomputed_",

packages/docs/src/routes/api/qwik/index.mdx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,27 @@ Expression which should be lazy loaded
119119

120120
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/qrl/qrl.public.ts)
121121

122+
## AsyncComputedFn
123+
124+
```typescript
125+
export type AsyncComputedFn<T> = (ctx: TaskCtx) => Promise<T>;
126+
```
127+
128+
**References:** [TaskCtx](#taskctx)
129+
130+
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-async-computed.ts)
131+
132+
## AsyncComputedReturnType
133+
134+
```typescript
135+
export type AsyncComputedReturnType<T> =
136+
T extends Promise<infer T> ? ReadonlySignal<T> : ReadonlySignal<T>;
137+
```
138+
139+
**References:** [ReadonlySignal](#readonlysignal)
140+
141+
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-async-computed.ts)
142+
122143
## cache
123144

124145
```typescript
@@ -8444,6 +8465,47 @@ T
84448465
84458466
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/reactive-primitives/impl/store.ts)
84468467
8468+
## useAsyncComputed$
8469+
8470+
Creates a computed signal which is calculated from the given function. A computed signal is a signal which is calculated from other signals. When the signals change, the computed signal is recalculated, and if the result changed, all tasks which are tracking the signal will be re-run and all components that read the signal will be re-rendered.
8471+
8472+
The function must be synchronous and must not have any side effects.
8473+
8474+
```typescript
8475+
useAsyncComputed$: <T>(qrl: AsyncComputedFn<T>) => AsyncComputedReturnType<T>;
8476+
```
8477+
8478+
<table><thead><tr><th>
8479+
8480+
Parameter
8481+
8482+
</th><th>
8483+
8484+
Type
8485+
8486+
</th><th>
8487+
8488+
Description
8489+
8490+
</th></tr></thead>
8491+
<tbody><tr><td>
8492+
8493+
qrl
8494+
8495+
</td><td>
8496+
8497+
[AsyncComputedFn](#asynccomputedfn)&lt;T&gt;
8498+
8499+
</td><td>
8500+
8501+
</td></tr>
8502+
</tbody></table>
8503+
**Returns:**
8504+
8505+
[AsyncComputedReturnType](#asynccomputedreturntype)&lt;T&gt;
8506+
8507+
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-async-computed.ts)
8508+
84478509
## useComputed$
84488510
84498511
Creates a computed signal which is calculated from the given function. A computed signal is a signal which is calculated from other signals. When the signals change, the computed signal is recalculated, and if the result changed, all tasks which are tracking the signal will be re-run and all components that read the signal will be re-rendered.

packages/qwik/public.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export {
5656
TaskCtx,
5757
// TODO do we really want to export this?
5858
untrack,
59+
useAsyncComputed$,
5960
useComputed$,
6061
useConstant,
6162
useContext,

packages/qwik/src/core/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ export { useTaskQrl } from './use/use-task';
133133
export { useTask$ } from './use/use-task-dollar';
134134
export { useVisibleTask$ } from './use/use-visible-task-dollar';
135135
export { useComputed$ } from './use/use-computed';
136+
export type { AsyncComputedFn, AsyncComputedReturnType } from './use/use-async-computed';
137+
export { useAsyncComputedQrl, useAsyncComputed$ } from './use/use-async-computed';
136138
export { useErrorBoundary } from './use/use-error-boundary';
137139
export type { ErrorBoundaryStore } from './shared/error/error-handling';
138140
export {

packages/qwik/src/core/qwik.core.api.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ import { ValueOrPromise as ValueOrPromise_2 } from '..';
1616
// @public
1717
export const $: <T>(expression: T) => QRL<T>;
1818

19+
// @public (undocumented)
20+
export type AsyncComputedFn<T> = (ctx: TaskCtx) => Promise<T>;
21+
22+
// @public (undocumented)
23+
export type AsyncComputedReturnType<T> = T extends Promise<infer T> ? ReadonlySignal<T> : ReadonlySignal<T>;
24+
1925
// @public
2026
export type ClassList = string | undefined | null | false | Record<string, boolean | string | number | null | undefined> | ClassList[];
2127

@@ -1615,6 +1621,14 @@ export const untrack: <T>(fn: () => T) => T;
16151621
// @public
16161622
export const unwrapStore: <T>(value: T) => T;
16171623

1624+
// @public
1625+
export const useAsyncComputed$: <T>(qrl: AsyncComputedFn<T>) => AsyncComputedReturnType<T>;
1626+
1627+
// Warning: (ae-internal-missing-underscore) The name "useAsyncComputedQrl" should be prefixed with an underscore because the declaration is marked as @internal
1628+
//
1629+
// @internal (undocumented)
1630+
export const useAsyncComputedQrl: <T>(qrl: QRL<AsyncComputedFn<T>>) => AsyncComputedReturnType<T>;
1631+
16181632
// @public
16191633
export const useComputed$: <T>(qrl: ComputedFn<T>) => ComputedReturnType<T>;
16201634

packages/qwik/src/core/reactive-primitives/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ export const triggerEffects = (
9797
let choreType = ChoreType.TASK;
9898
if (consumer.$flags$ & TaskFlags.VISIBLE_TASK) {
9999
choreType = ChoreType.VISIBLE;
100+
} else if (consumer.$flags$ & TaskFlags.ASYNC_COMPUTED) {
101+
choreType = ChoreType.ASYNC_COMPUTED;
100102
}
101103
container.$scheduler$(choreType, consumer);
102104
} else if (consumer instanceof SignalImpl) {

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ import type { ValueOrPromise } from './utils/types';
122122
import type { NodePropPayload } from '../reactive-primitives/subscription-data';
123123
import type { ComputedSignalImpl } from '../reactive-primitives/impl/computed-signal-impl';
124124
import type { WrappedSignalImpl } from '../reactive-primitives/impl/wrapped-signal-impl';
125+
import { runAsyncComputed } from '../use/use-async-computed';
125126

126127
// Turn this on to get debug output of what the scheduler is doing.
127128
const DEBUG: boolean = false;
@@ -186,7 +187,10 @@ export const createScheduler = (
186187
host: HostElement | null,
187188
target: Signal
188189
): ValueOrPromise<void>;
189-
function schedule(type: ChoreType.TASK | ChoreType.VISIBLE, task: Task): ValueOrPromise<void>;
190+
function schedule(
191+
type: ChoreType.TASK | ChoreType.VISIBLE | ChoreType.ASYNC_COMPUTED,
192+
task: Task
193+
): ValueOrPromise<void>;
190194
function schedule(
191195
type: ChoreType.RUN_QRL,
192196
host: HostElement,
@@ -225,7 +229,10 @@ export const createScheduler = (
225229
const runLater: boolean =
226230
type !== ChoreType.WAIT_FOR_ALL && !isComponentSsr && type !== ChoreType.RUN_QRL;
227231
const isTask =
228-
type === ChoreType.TASK || type === ChoreType.VISIBLE || type === ChoreType.CLEANUP_VISIBLE;
232+
type === ChoreType.TASK ||
233+
type === ChoreType.VISIBLE ||
234+
type === ChoreType.ASYNC_COMPUTED ||
235+
type === ChoreType.CLEANUP_VISIBLE;
229236
const isClientOnly =
230237
type === ChoreType.JOURNAL_FLUSH ||
231238
type === ChoreType.NODE_DIFF ||
@@ -400,6 +407,7 @@ export const createScheduler = (
400407
break;
401408
case ChoreType.TASK:
402409
case ChoreType.VISIBLE:
410+
case ChoreType.ASYNC_COMPUTED:
403411
{
404412
const payload = chore.$payload$ as DescriptorBase;
405413
if (payload.$flags$ & TaskFlags.RESOURCE) {
@@ -411,6 +419,8 @@ export const createScheduler = (
411419
// Awaiting on the client also causes a deadlock.
412420
// In any case, the resource will never throw.
413421
returnValue = isServer ? result : null;
422+
} else if (chore.$type$ === ChoreType.ASYNC_COMPUTED) {
423+
returnValue = runAsyncComputed(payload as Task<TaskFn, TaskFn>, container, host);
414424
} else {
415425
returnValue = runTask(payload as Task<TaskFn, TaskFn>, container, host);
416426
}

packages/qwik/src/core/shared/util-chore-type.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const enum ChoreType {
77
/** Ensure that the QRL promise is resolved before processing next chores in the queue */
88
QRL_RESOLVE /* ********************** */ = 1,
99
RUN_QRL,
10+
ASYNC_COMPUTED,
1011
TASK,
1112
NODE_DIFF,
1213
NODE_PROP,

packages/qwik/src/core/shared/utils/markers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export const SVG_NS = 'http://www.w3.org/2000/svg';
5656
export const MATH_NS = 'http://www.w3.org/1998/Math/MathML';
5757

5858
export const ResourceEvent = 'qResource';
59-
export const ComputedEvent = 'qComputed';
59+
export const AsyncComputedEvent = 'qAsyncComputed';
6060
export const RenderEvent = 'qRender';
6161
export const TaskEvent = 'qTask';
6262

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { Fragment as Signal, component$, useSignal } from '@qwik.dev/core';
2+
import { domRender, ssrRenderToDom, trigger } from '@qwik.dev/core/testing';
3+
import { describe, expect, it } from 'vitest';
4+
import { useAsyncComputed$ } from '../use/use-async-computed';
5+
6+
const debug = false; //true;
7+
Error.stackTraceLimit = 100;
8+
9+
describe.each([
10+
{ render: ssrRenderToDom }, //
11+
{ render: domRender }, //
12+
])('$render.name: useComputed', ({ render }) => {
13+
it('should resolve promise in computed result', async () => {
14+
const Counter = component$(() => {
15+
const count = useSignal(1);
16+
const doubleCount = useAsyncComputed$(({ track }) =>
17+
Promise.resolve(track(() => count.value * 2))
18+
);
19+
return <button onClick$={() => count.value++}>{doubleCount.value}</button>;
20+
});
21+
const { vNode, container } = await render(<Counter />, { debug });
22+
expect(vNode).toMatchVDOM(
23+
<>
24+
<button>
25+
<Signal ssr-required>{'2'}</Signal>
26+
</button>
27+
</>
28+
);
29+
await trigger(container.element, 'button', 'click');
30+
expect(vNode).toMatchVDOM(
31+
<>
32+
<button>
33+
<Signal ssr-required>{'4'}</Signal>
34+
</button>
35+
</>
36+
);
37+
});
38+
39+
it('should resolve delayed promise in computed result', async () => {
40+
const Counter = component$(() => {
41+
const count = useSignal(1);
42+
const doubleCount = useAsyncComputed$(
43+
({ track }) =>
44+
new Promise<number>((resolve) => {
45+
setTimeout(() => {
46+
resolve(track(() => count.value * 2));
47+
});
48+
})
49+
);
50+
return <button onClick$={() => count.value++}>{doubleCount.value}</button>;
51+
});
52+
const { vNode, container } = await render(<Counter />, { debug });
53+
54+
expect(vNode).toMatchVDOM(
55+
<>
56+
<button>
57+
<Signal ssr-required>{'2'}</Signal>
58+
</button>
59+
</>
60+
);
61+
await trigger(container.element, 'button', 'click');
62+
expect(vNode).toMatchVDOM(
63+
<>
64+
<button>
65+
<Signal ssr-required>{'4'}</Signal>
66+
</button>
67+
</>
68+
);
69+
});
70+
});

0 commit comments

Comments
 (0)