Skip to content

Commit 2c78fd9

Browse files
committed
feat: implement useAsyncComputed$
1 parent 9fce25a commit 2c78fd9

File tree

13 files changed

+376
-20
lines changed

13 files changed

+376
-20
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",
@@ -2120,6 +2148,20 @@
21202148
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/reactive-primitives/impl/store.ts",
21212149
"mdFile": "core.unwrapstore.md"
21222150
},
2151+
{
2152+
"name": "useAsyncComputed$",
2153+
"id": "useasynccomputed_",
2154+
"hierarchy": [
2155+
{
2156+
"name": "useAsyncComputed$",
2157+
"id": "useasynccomputed_"
2158+
}
2159+
],
2160+
"kind": "Function",
2161+
"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;",
2162+
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-async-computed.ts",
2163+
"mdFile": "core.useasynccomputed_.md"
2164+
},
21232165
{
21242166
"name": "useComputed$",
21252167
"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
@@ -8433,6 +8454,47 @@ T
84338454
84348455
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/reactive-primitives/impl/store.ts)
84358456
8457+
## useAsyncComputed$
8458+
8459+
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.
8460+
8461+
The function must be synchronous and must not have any side effects.
8462+
8463+
```typescript
8464+
useAsyncComputed$: <T>(qrl: AsyncComputedFn<T>) => AsyncComputedReturnType<T>;
8465+
```
8466+
8467+
<table><thead><tr><th>
8468+
8469+
Parameter
8470+
8471+
</th><th>
8472+
8473+
Type
8474+
8475+
</th><th>
8476+
8477+
Description
8478+
8479+
</th></tr></thead>
8480+
<tbody><tr><td>
8481+
8482+
qrl
8483+
8484+
</td><td>
8485+
8486+
[AsyncComputedFn](#asynccomputedfn)&lt;T&gt;
8487+
8488+
</td><td>
8489+
8490+
</td></tr>
8491+
</tbody></table>
8492+
**Returns:**
8493+
8494+
[AsyncComputedReturnType](#asynccomputedreturntype)&lt;T&gt;
8495+
8496+
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-async-computed.ts)
8497+
84368498
## useComputed$
84378499
84388500
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: 19 additions & 13 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

@@ -74,6 +80,9 @@ export const componentQrl: <PROPS extends Record<any, any>>(componentQrl: QRL<On
7480
// @public (undocumented)
7581
export type ComputedFn<T> = () => T;
7682

83+
// @public (undocumented)
84+
export type ComputedReturnType<T> = T extends Promise<infer T> ? ReadonlySignal<T> : ReadonlySignal<T>;
85+
7786
// @public
7887
export interface ComputedSignal<T> extends ReadonlySignal<T> {
7988
force(): void;
@@ -243,9 +252,6 @@ class DomContainer extends _SharedContainer implements ClientContainer {
243252
export { DomContainer }
244253
export { DomContainer as _DomContainer }
245254

246-
// @internal (undocumented)
247-
export const _dumpState: (state: unknown[], color?: boolean, prefix?: string, limit?: number | null) => string;
248-
249255
// @internal (undocumented)
250256
export const _EFFECT_BACK_REF: unique symbol;
251257

@@ -551,11 +557,6 @@ export const PrefetchServiceWorker: (opts: {
551557
nonce?: string;
552558
}) => JSXOutput;
553559

554-
// Warning: (ae-forgotten-export) The symbol "DeserializeContainer" needs to be exported by the entry point index.d.ts
555-
//
556-
// @internal
557-
export function _preprocessState(data: unknown[], container: DeserializeContainer): void;
558-
559560
// @public
560561
export type PropFunction<T> = QRL<T>;
561562

@@ -1621,12 +1622,20 @@ export const untrack: <T>(fn: () => T) => T;
16211622
export const unwrapStore: <T>(value: T) => T;
16221623

16231624
// @public
1624-
export const useComputed$: <T>(qrl: ComputedFn<T>) => T extends Promise<any> ? never : ReadonlySignal<T>;
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+
1632+
// @public
1633+
export const useComputed$: <T>(qrl: ComputedFn<T>) => ComputedReturnType<T>;
16251634

16261635
// Warning: (ae-internal-missing-underscore) The name "useComputedQrl" should be prefixed with an underscore because the declaration is marked as @internal
16271636
//
16281637
// @internal (undocumented)
1629-
export const useComputedQrl: <T>(qrl: QRL<ComputedFn<T>>) => T extends Promise<any> ? never : ReadonlySignal<T>;
1638+
export const useComputedQrl: <T>(qrl: QRL<ComputedFn<T>>) => ComputedReturnType<T>;
16301639

16311640
// @public
16321641
export const useConstant: <T>(value: (() => T) | T) => T;
@@ -1779,9 +1788,6 @@ export type VisibleTaskStrategy = 'intersection-observer' | 'document-ready' | '
17791788
// @internal (undocumented)
17801789
export type _VNode = _ElementVNode | _TextVNode | _VirtualVNode;
17811790

1782-
// @internal (undocumented)
1783-
export function _vnode_toString(this: _VNode | null, depth?: number, offset?: string, materialize?: boolean, siblings?: boolean, colorize?: boolean): string;
1784-
17851791
// @internal
17861792
export const enum _VNodeFlags {
17871793
// (undocumented)

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)