Skip to content

packages/core/src/features/viz/inspection/handler.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 type { PibblEvent } from "@pibbl/core";
2 import {
3   borrowDispatchResource,
4   withHooksForbidden,
5 } from "@pibbl/core/internal";
6 
7 /** A mount-owned source of borrowed committed render records.
8  * @see {@link InspectionContext}
9  */
10 export interface InspectionRef<S> {
11   /** Returns the current committed record.
12    * @returns Borrowed record or undefined before paint/after disposal.
13    */
14   rendered(): S | undefined;
15 }
16 
17 /** Lazy synchronous inspection; expires when its handler returns.
18  * @see {@link InspectionRef}
19  */
20 export interface InspectionContext {
21   /** Pins one receiver's committed generation for this logical dispatch.
22    * @param ref - Explicit ref identifying the mounted receiver.
23    * @returns Borrowed generation or undefined before paint/after disposal.
24    */
25   rendered<S>(ref: InspectionRef<S>): S | undefined;
26   /**
27    * Executes only on demand and memoizes equal requests, including no result.
28    * Options contain plain scalar fields, function identities and logical points.
29    * @param query - Pure synchronous query with a stable result contract.
30    * @param rendered - Committed generation pinned for this request.
31    * @param options - Named query parameters; functions compare by identity.
32    * @returns Query result, including undefined, reused for equal requests in dispatch.
33    */
34   request<S, A, R>(
35     query: (rendered: S, options: A) => R,
36     rendered: S,
37     options: A,
38   ): R;
39 }
40 
41 const dispatchKey = {};
42 interface Memo {
43   query: unknown;
44   rendered: unknown;
45   options: unknown;
46   result: unknown;
47 }
48 interface DispatchInspection {
49   live: boolean;
50   renders: Map<object, unknown>;
51   memo: Memo[];
52 }
53 
54 function copyArguments(value: unknown, depth = 0): unknown {
55   if (value === null || typeof value !== "object") return value;
56   if (depth > 2 || Object.getPrototypeOf(value) !== Object.prototype)
57     throw new TypeError(
58       "Inspection arguments must contain plain scalar options and logical points.",
59     );
60   return Object.fromEntries(
61     Object.entries(value).map(([key, item]) => [
62       key,
63       copyArguments(item, depth + 1),
64     ]),
65   );
66 }
67 function sameArguments(a: unknown, b: unknown): boolean {
68   if (Object.is(a, b)) return true;
69   if (!a || !b || typeof a !== "object" || typeof b !== "object") return false;
70   const aa = a as Record<string, unknown>,
71     bb = b as Record<string, unknown>;
72   const keys = Object.keys(aa);
73   return (
74     keys.length === Object.keys(bb).length &&
75     keys.every(
76       (key) => Object.hasOwn(bb, key) && sameArguments(aa[key], bb[key]),
77     )
78   );
79 }
80 
81 /**
82  * Adapts an ordinary handler to lazy typed inspection without changing its event.
83  * @param handler - Synchronous callback receiving the unchanged event and an expiring context.
84  * @returns An ordinary event handler. Searches occur only on request; memo state releases at dispatch end.
85  * @see {@link InspectionContext}
86  */
87 export function withInspection<E extends PibblEvent>(
88   handler: (event: E, context: InspectionContext) => void,
89 ): (event: E) => void {
90   return (event) => {
91     let live = true;
92     const borrow = () => {
93       if (!live)
94         throw new Error(
95           "Inspection context has expired; use it synchronously within dispatch.",
96         );
97       return borrowDispatchResource<DispatchInspection>(
98         event,
99         dispatchKey,
100         () => {
101           const value: DispatchInspection = {
102             live: true,
103             renders: new Map(),
104             memo: [],
105           };
106           return {
107             value,
108             dispose: () => {
109               value.live = false;
110               value.renders.clear();
111               value.memo.length = 0;
112             },
113           };
114         },
115       );
116     };
117     const context: InspectionContext = Object.freeze({
118       rendered<S>(ref: InspectionRef<S>): S | undefined {
119         const state = borrow();
120         if (!state.renders.has(ref)) state.renders.set(ref, ref.rendered());
121         return state.renders.get(ref) as S | undefined;
122       },
123       request<S, A, R>(
124         query: (rendered: S, options: A) => R,
125         rendered: S,
126         options: A,
127       ): R {
128         const state = borrow();
129         const args = copyArguments(options);
130         const cached = state.memo.find(
131           (memo) =>
132             memo.query === query &&
133             memo.rendered === rendered &&
134             sameArguments(memo.options, args),
135         );
136         if (cached) return cached.result as R;
137         const result = withHooksForbidden(
138           "Inspection queries must be pure synchronous functions.",
139           () => query(rendered, options),
140         );
141         if (result && typeof (result as { then?: unknown }).then === "function")
142           throw new TypeError("Inspection queries must be synchronous.");
143         state.memo.push({ query, rendered, options: args, result });
144         return result;
145       },
146     });
147     try {
148       const result = handler(event, context) as unknown;
149       if (result && typeof (result as { then?: unknown }).then === "function")
150         throw new TypeError("Inspection handlers must be synchronous.");
151     } finally {
152       live = false;
153     }
154   };
155 }
156 

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