|
| 1 | +import { qwikDebugToString } from '../../debug'; |
| 2 | +import { QError, qError } from '../../shared/error/error'; |
| 3 | +import type { Container } from '../../shared/types'; |
| 4 | +import { ChoreType } from '../../shared/util-chore-type'; |
| 5 | +import { isPromise } from '../../shared/utils/promises'; |
| 6 | +import { invoke, newInvokeContext } from '../../use/use-core'; |
| 7 | +import { isSignal, throwIfQRLNotResolved } from '../utils'; |
| 8 | +import type { BackRef } from '../cleanup'; |
| 9 | +import { getSubscriber } from '../subscriber'; |
| 10 | +import type { AsyncComputeQRL, EffectSubscription } from '../types'; |
| 11 | +import { _EFFECT_BACK_REF, EffectProperty, SignalFlags, STORE_ALL_PROPS } from '../types'; |
| 12 | +import { addStoreEffect, getStoreHandler, getStoreTarget, isStore } from './store'; |
| 13 | +import type { Signal } from '../signal.public'; |
| 14 | +import { isFunction } from '../../shared/utils/types'; |
| 15 | +import { ComputedSignalImpl } from './computed-signal-impl'; |
| 16 | +import { setupSignalValueAccess } from './signal-impl'; |
| 17 | +import { isDev } from '@qwik.dev/core/build'; |
| 18 | + |
| 19 | +const DEBUG = false; |
| 20 | +const log = (...args: any[]) => |
| 21 | + // eslint-disable-next-line no-console |
| 22 | + console.log('ASYNC COMPUTED SIGNAL', ...args.map(qwikDebugToString)); |
| 23 | + |
| 24 | +export class AsyncComputedSignalImpl<T> |
| 25 | + extends ComputedSignalImpl<T, AsyncComputeQRL<T>> |
| 26 | + implements BackRef |
| 27 | +{ |
| 28 | + $untrackedPending$: boolean = false; |
| 29 | + $untrackedError$: Error | null = null; |
| 30 | + |
| 31 | + $pendingEffects$: null | Set<EffectSubscription> = null; |
| 32 | + $errorEffects$: null | Set<EffectSubscription> = null; |
| 33 | + private $promiseValue$: T | null = null; |
| 34 | + |
| 35 | + [_EFFECT_BACK_REF]: Map<EffectProperty | string, EffectSubscription> | null = null; |
| 36 | + |
| 37 | + constructor(container: Container | null, fn: AsyncComputeQRL<T>, flags = SignalFlags.INVALID) { |
| 38 | + super(container, fn, flags); |
| 39 | + } |
| 40 | + |
| 41 | + get pending(): boolean { |
| 42 | + return setupSignalValueAccess( |
| 43 | + this, |
| 44 | + () => (this.$pendingEffects$ ||= new Set()), |
| 45 | + () => this.untrackedPending |
| 46 | + ); |
| 47 | + } |
| 48 | + |
| 49 | + set untrackedPending(value: boolean) { |
| 50 | + if (value !== this.$untrackedPending$) { |
| 51 | + this.$untrackedPending$ = value; |
| 52 | + this.$container$?.$scheduler$( |
| 53 | + ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS, |
| 54 | + null, |
| 55 | + this, |
| 56 | + this.$pendingEffects$ |
| 57 | + ); |
| 58 | + } |
| 59 | + } |
| 60 | + |
| 61 | + get untrackedPending() { |
| 62 | + return this.$untrackedPending$; |
| 63 | + } |
| 64 | + |
| 65 | + get error(): Error | null { |
| 66 | + return setupSignalValueAccess( |
| 67 | + this, |
| 68 | + () => (this.$errorEffects$ ||= new Set()), |
| 69 | + () => this.untrackedError |
| 70 | + ); |
| 71 | + } |
| 72 | + |
| 73 | + set untrackedError(value: Error | null) { |
| 74 | + if (value !== this.$untrackedError$) { |
| 75 | + this.$untrackedError$ = value; |
| 76 | + this.$container$?.$scheduler$( |
| 77 | + ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS, |
| 78 | + null, |
| 79 | + this, |
| 80 | + this.$errorEffects$ |
| 81 | + ); |
| 82 | + } |
| 83 | + } |
| 84 | + |
| 85 | + get untrackedError() { |
| 86 | + return this.$untrackedError$; |
| 87 | + } |
| 88 | + |
| 89 | + $computeIfNeeded$() { |
| 90 | + if (!(this.$flags$ & SignalFlags.INVALID)) { |
| 91 | + return false; |
| 92 | + } |
| 93 | + const computeQrl = this.$computeQrl$; |
| 94 | + throwIfQRLNotResolved(computeQrl); |
| 95 | + |
| 96 | + const untrackedValue = |
| 97 | + this.$promiseValue$ ?? (computeQrl.getFn()({ track: this.$trackFn$.bind(this) }) as T); |
| 98 | + if (isPromise(untrackedValue)) { |
| 99 | + this.untrackedPending = true; |
| 100 | + this.untrackedError = null; |
| 101 | + throw untrackedValue |
| 102 | + .then((promiseValue) => { |
| 103 | + this.$promiseValue$ = promiseValue; |
| 104 | + this.untrackedPending = false; |
| 105 | + }) |
| 106 | + .catch((err) => { |
| 107 | + if (isDev) { |
| 108 | + console.error(err); |
| 109 | + } |
| 110 | + this.untrackedError = err; |
| 111 | + }); |
| 112 | + } |
| 113 | + this.$promiseValue$ = null; |
| 114 | + DEBUG && log('Signal.$asyncCompute$', untrackedValue); |
| 115 | + |
| 116 | + this.$flags$ &= ~SignalFlags.INVALID; |
| 117 | + |
| 118 | + const didChange = untrackedValue !== this.$untrackedValue$; |
| 119 | + if (didChange) { |
| 120 | + this.$untrackedValue$ = untrackedValue; |
| 121 | + } |
| 122 | + return didChange; |
| 123 | + } |
| 124 | + |
| 125 | + private $trackFn$(obj: (() => unknown) | object | Signal<unknown>, prop?: string) { |
| 126 | + const ctx = newInvokeContext(); |
| 127 | + ctx.$effectSubscriber$ = getSubscriber(this, EffectProperty.VNODE); |
| 128 | + ctx.$container$ = this.$container$ || undefined; |
| 129 | + return invoke(ctx, () => { |
| 130 | + if (isFunction(obj)) { |
| 131 | + return obj(); |
| 132 | + } |
| 133 | + if (prop) { |
| 134 | + return (obj as Record<string, unknown>)[prop]; |
| 135 | + } else if (isSignal(obj)) { |
| 136 | + return obj.value; |
| 137 | + } else if (isStore(obj)) { |
| 138 | + // track whole store |
| 139 | + addStoreEffect( |
| 140 | + getStoreTarget(obj)!, |
| 141 | + STORE_ALL_PROPS, |
| 142 | + getStoreHandler(obj)!, |
| 143 | + ctx.$effectSubscriber$! |
| 144 | + ); |
| 145 | + return obj; |
| 146 | + } else { |
| 147 | + throw qError(QError.trackObjectWithoutProp); |
| 148 | + } |
| 149 | + }); |
| 150 | + } |
| 151 | +} |
0 commit comments