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