packages/core/src/lib/hooks/use-computed.ts
This is the source snapshot used to build these API details. View this revision on GitHub.
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 version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.