Skip to content

packages/core/src/lib/hooks/use-computed.ts

Read as Markdown

This is the source snapshot used to build these API details. View this revision on GitHub.

Back to reference

1 import { GLOBAL_STATE } from '../global-state.js';
2 import type { PibblComponentRefs, PibblInstance, PibblRenderTransaction } from '../types.js';
3 import {
4   adoptComputedFormulaCandidate,
5   computed,
6   createComputedFormulaCandidate,
7   createComputedSignalAlias,
8   readComputedFormulaCandidate,
9   replaceComputedFormula,
10   type ComputedFormulaCandidate,
11 } from '../signals/graph.js';
12 import type { Signal, SignalOptions } from '../signals/types.js';
13 import {
14   requireHookContext,
15   useHookSlot,
16   withHooksForbidden,
17 } from './hook-slot.js';
18 
19 interface PibblComputedHookSlot<T> {
20   readonly candidates: WeakMap<
21     PibblRenderTransaction,
22     ComputedFormulaCandidate<T>
23   >;
24   readonly componentRefs: PibblComponentRefs<any>;
25   readonly pibblInstance: PibblInstance;
26   dependencies: readonly unknown[] | undefined;
27   readonly output: Signal<T>;
28   readonly signal: Signal<T>;
29   renderRevision: number;
30 }
31 
32 /**
33  * Creates one component-owned lazy computed signal for a mounted hook slot.
34  *
35  * @param compute - Synchronous calculation whose signal reads are tracked.
36  * @param dependencies - Values controlling replacement of the captured computation.
37  * @param options - Equality comparison and optional diagnostic name. See {@link SignalOptions}.
38  * @returns A mount-owned readonly computed signal. See {@link Signal}.
39  *
40  * @see {@link SignalOptions}
41  * @see {@link Signal}
42  */
43 export function useComputed<T>(
44   compute: () => T,
45   dependencies?: readonly unknown[],
46   options?: SignalOptions<T>,
47 ): Signal<T> {
48   const { componentRefs, pibblInstance } = requireHookContext();
49   const formula = guardedFormula(compute);
50   const dependencySnapshot = snapshotDependencies(dependencies);
51   const slot = useHookSlot<PibblComputedHookSlot<T>>('computed', () => {
52     const output = computed(formula, options);
53     const candidates = new WeakMap<
54       PibblRenderTransaction,
55       ComputedFormulaCandidate<T>
56     >();
57     const signal = createComputedSignalAlias(output, () => {
58       const transaction = GLOBAL_STATE.renderTransaction;
59       const candidate = transaction === undefined ? undefined :
60         candidates.get(transaction);
61       return candidate === undefined ? output.get() :
62         readComputedFormulaCandidate(output, candidate);
63     });
64     return {
65       candidates,
66       componentRefs,
67       pibblInstance,
68       dependencies: dependencySnapshot,
69       output,
70       signal,
71       renderRevision: 0,
72     };
73   }).value;
74 
75   const transaction = GLOBAL_STATE.renderTransaction;
76   if (!transaction) {
77     throw new Error('useComputed requires an active Pibbl render transaction.');
78   }
79   const dependenciesChanged = !dependencySnapshotsEqual(
80     slot.dependencies,
81     dependencySnapshot,
82   );
83   const candidate = dependenciesChanged ?
84     createComputedFormulaCandidate(slot.output, formula) : undefined;
85   if (candidate !== undefined) slot.candidates.set(transaction, candidate);
86 
87   const revision = ++slot.renderRevision;
88   transaction.onFinish(success => {
89     if (
90       candidate !== undefined &&
91       slot.candidates.get(transaction) === candidate
92     ) {
93       slot.candidates.delete(transaction);
94     }
95     if (!success || revision !== slot.renderRevision || !isCurrentSlot(slot)) {
96       return;
97     }
98     if (candidate === undefined) {
99       replaceComputedFormula(slot.output, formula, false);
100       return;
101     }
102 
103     slot.dependencies = dependencySnapshot;
104     if (candidate.evaluated) {
105       adoptComputedFormulaCandidate(slot.output, candidate, formula);
106     } else {
107       replaceComputedFormula(slot.output, formula, true);
108     }
109   });
110   return slot.signal;
111 }
112 
113 function guardedFormula<T>(compute: () => T): () => T {
114   return () => withHooksForbidden(
115     'Pibbl hooks cannot be called inside a useComputed formula.',
116     compute,
117   );
118 }
119 
120 function snapshotDependencies(
121   dependencies: readonly unknown[] | undefined,
122 ): readonly unknown[] | undefined {
123   return dependencies === undefined ? undefined : [...dependencies];
124 }
125 
126 function dependencySnapshotsEqual(
127   previous: readonly unknown[] | undefined,
128   next: readonly unknown[] | undefined,
129 ): boolean {
130   if (previous === undefined || next === undefined) return previous === next;
131   if (previous.length !== next.length) return false;
132   for (let index = 0; index < previous.length; index++) {
133     if (!Object.is(previous[index], next[index])) return false;
134   }
135   return true;
136 }
137 
138 function isCurrentSlot(slot: PibblComputedHookSlot<unknown>): boolean {
139   return !slot.componentRefs.teardowns.closed &&
140     !slot.pibblInstance.mainTeardowns.closed &&
141     GLOBAL_STATE.pibblInstances.get(slot.pibblInstance.canvas) === slot.pibblInstance &&
142     slot.pibblInstance.refs.get(slot.componentRefs.id) === slot.componentRefs;
143 }
144 

Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.