Skip to content

Commit 132d73c

Browse files
committed
Add handler closure transformation infrastructure
1 parent 381e6a2 commit 132d73c

18 files changed

+904
-43
lines changed
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Handler Closures Transformation Spec
2+
3+
## Background
4+
5+
- The `dx1/unify-types` branch introduces a unified cell/stream type system
6+
built on the new `CELL_BRAND` symbol and capability interfaces (`IReadable`,
7+
`IWritable`, `IKeyable`, etc.).
8+
- API exports and `commontools.d.ts` now define `HandlerFunction` in terms of
9+
`HandlerState<T>` and `StripCell<T>`, and the runner augments these interfaces
10+
with runtime-specific behaviour.
11+
- Schema generator and opaque-ref transformers detect cell kinds via
12+
`CELL_BRAND`, keeping the hierarchical capture utilities valid across
13+
transformers.
14+
15+
## Impact on Handler Closures
16+
17+
- Generated handler callbacks must follow the public signature `(event, props)`
18+
so that `HandlerState<T>` inference works and schema injection can attach
19+
`toSchema` calls automatically.
20+
- Captured values should appear under the `params` object we pass to
21+
`handler(...)`; we can reuse the hierarchical capture tree helpers we built
22+
for map closures to keep names/structure intact.
23+
- Inline closures should only be rewritten when they are literal functions
24+
supplied to `on*` JSX attributes and are not already wrapped in
25+
`handler(...)`.
26+
27+
## Behaviour Overview
28+
29+
- Detect eligible JSX attributes using the existing `isEventHandlerJsxAttribute`
30+
helper (prop name starts with `on`).
31+
- For inline arrow functions transform
32+
```tsx
33+
<button onClick={() => counter.set(counter.get() + 1)} type="button" />;
34+
```
35+
into
36+
```tsx
37+
<button
38+
onClick={handler((event, { counter }) => counter.set(counter.get() + 1))({
39+
counter,
40+
})}
41+
type="button"
42+
/>;
43+
```
44+
preserving user-supplied parameter patterns whenever present.
45+
- The params object mirrors the capture tree: nested state (`state.user.name`)
46+
becomes `{ state: { user: { name: state.user.name } } }` and optional
47+
chaining/computed keys are left untouched.
48+
- Inline handlers are rewritten even when no captures exist. This keeps JSX
49+
syntax uniform and avoids downstream behaviour changes when captures are
50+
introduced later.
51+
- Schema injection already recognises `handler(...)` calls and inserts
52+
`toSchema` arguments, so no additional transformer work is required there.
53+
54+
## Implementation Summary
55+
56+
1. Extend `ClosureTransformer` with a visitor that:
57+
- Finds JSX attributes whose name starts with `on`.
58+
- Confirms the initializer is an inline arrow function literal (excluding
59+
existing `handler(...)` invocations or other non-inline references).
60+
- Reuses `collectCaptures` / `groupCapturesByRoot` to build the capture tree
61+
for the handler body.
62+
2. Build the rewritten callback:
63+
- Preserve explicit event/state parameters if the user provided them;
64+
otherwise synthesise `(eventParam, paramsParam)` placeholders.
65+
- Alias runtime names to originals via destructuring (e.g.
66+
`({ params: { counter } })`).
67+
- Leave the function body untouched except for computed-key caching
68+
(identical to map closures).
69+
3. Emit the `handler(...)` call and params object; rely on schema injection to
70+
add `toSchema` arguments.
71+
4. Capture regression coverage for: a basic increment handler; optional
72+
chaining/captured state; computed property access (`list[nextKey()]`); and an
73+
outer-variable collision (`const counter = …; onClick={() => counter}`).
74+
5. Run `deno lint` and `deno task test` to cover the closure, derive, and
75+
handler suites.
76+
77+
## Decisions and Constraints
78+
79+
- We focus on inline arrow functions for now; other inline forms can be handled
80+
later if necessary.
81+
- If the attribute already references a value returned from `handler(...)` (an
82+
`OpaqueRef`/cell) or any non-inline identifier, we leave it as-is.
83+
- Captured cells/streams/opaque refs require no special casing—the capture tree
84+
handles them generically.
85+
86+
## Examples
87+
88+
### Basic Capture
89+
90+
```tsx
91+
// Before
92+
<button onClick={() => state.counter.set(state.counter.get() + 1)} type="button" />
93+
94+
// After
95+
<button
96+
type="button"
97+
onClick={handler((event, { counter }) => counter.set(counter.get() + 1))({
98+
counter,
99+
})}
100+
/>
101+
```
102+
103+
### Optional Chaining and Computed Key
104+
105+
```tsx
106+
// Before
107+
<button
108+
type="button"
109+
onClick={() => recordMap[nextKey()]?.set(state.metrics.get() ?? 0)}
110+
/>
111+
112+
// After
113+
<button
114+
type="button"
115+
onClick={handler((event, { recordMap, state }) =>
116+
recordMap[nextKey()]?.set(state.metrics.get() ?? 0)
117+
)({
118+
recordMap,
119+
state: {
120+
metrics: state.metrics,
121+
},
122+
})}
123+
/>
124+
```

0 commit comments

Comments
 (0)