@echothink-ui/events 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -0
- package/dist/bridge.d.ts +48 -0
- package/dist/context.d.ts +43 -0
- package/dist/hooks.d.ts +34 -0
- package/dist/index.cjs +128 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +100 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +125 -0
- package/package.json +39 -0
- package/src/__tests__/events.test.tsx +159 -0
- package/src/bridge.ts +96 -0
- package/src/context.tsx +69 -0
- package/src/hooks.tsx +69 -0
- package/src/index.ts +42 -0
- package/src/types.ts +177 -0
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# @echothink-ui/events
|
|
2
|
+
|
|
3
|
+
The backend-event contract for `@echothink-ui` components.
|
|
4
|
+
|
|
5
|
+
`@echothink-ui` components expose plain React callbacks (`onClick`,
|
|
6
|
+
`EthAction.onSelect`, form `onSubmit`, table `rowActions`) with no path to a
|
|
7
|
+
backend process. This package provides the serializable binding shape and the
|
|
8
|
+
host-injected dispatch that carry those events to a backend (ultimately
|
|
9
|
+
`runProcess` → gateway), while staying structurally compatible with the
|
|
10
|
+
echothink-fugue registry `EventBinding` / action-graph contract.
|
|
11
|
+
|
|
12
|
+
## Pieces
|
|
13
|
+
|
|
14
|
+
- `EthEventBinding` / `EthAction` — the serializable "when this event fires, run
|
|
15
|
+
these actions" shape a builder authors. Action kinds: `runProcess`,
|
|
16
|
+
`navigate`, `setState`, `openModal`, `closeModal`, `showToast`.
|
|
17
|
+
- `EchoEventProvider` — the host injects a `dispatch(binding, payload, data)`
|
|
18
|
+
implementation here. A no-op default lets components render with no provider.
|
|
19
|
+
- `useEchoEvents()` — returns `{ dispatch }`.
|
|
20
|
+
- `useEthAction(binding)` — returns an `onSelect`/`onClick` handler to wire
|
|
21
|
+
`EthAction.onSelect` → dispatch (or `undefined` when unbound).
|
|
22
|
+
- `useEthEventHandler(binding)` — returns a payload-carrying handler for events
|
|
23
|
+
that produce live data (form values, selected row, chosen rowAction).
|
|
24
|
+
- `toRegistryEventBinding(binding)` — the edge adapter that renames the authoring
|
|
25
|
+
shape (`kind`/`payload`/`target`) to the registry action-graph shape
|
|
26
|
+
(`action`/`externalInput`/`route`/`modalId`/`key`).
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
import {
|
|
32
|
+
EchoEventProvider,
|
|
33
|
+
useEthAction,
|
|
34
|
+
toRegistryEventBinding,
|
|
35
|
+
type EthEventBinding,
|
|
36
|
+
} from "@echothink-ui/events";
|
|
37
|
+
|
|
38
|
+
// Host wires dispatch to the registry/renderer action runtime:
|
|
39
|
+
const dispatch = (binding, payload) => {
|
|
40
|
+
const reg = toRegistryEventBinding(binding);
|
|
41
|
+
pageRuntime.dispatch([reg], binding.event, { row: payload });
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
<EchoEventProvider dispatch={dispatch}>
|
|
45
|
+
<MyPage />
|
|
46
|
+
</EchoEventProvider>;
|
|
47
|
+
|
|
48
|
+
// Component wires a rich callback to a backend binding:
|
|
49
|
+
function ArchiveButton({ binding }: { binding?: EthEventBinding }) {
|
|
50
|
+
const onSelect = useEthAction(binding);
|
|
51
|
+
return <Button onClick={onSelect}>Archive</Button>;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
See `../echothink-UI-components/docs/backend-events.md` for the full design.
|
package/dist/bridge.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bridge` — the field-renaming adapter that turns an {@link EthEventBinding}
|
|
3
|
+
* (the UI-library shape, discriminant `kind`) into the echothink-fugue registry
|
|
4
|
+
* `EventBinding` shape (discriminant `action`) that
|
|
5
|
+
* `runActionGraph`/`runProcess` consume.
|
|
6
|
+
*
|
|
7
|
+
* This lives in the UI library (not the registry) so the UI side OWNS its
|
|
8
|
+
* authoring shape and the host's `dispatch` can be a thin `runActionGraph(
|
|
9
|
+
* toRegistryActions(binding.actions), ctx)`. It is the documented, single point
|
|
10
|
+
* where the two contracts meet — keeping the rest of `@echothink-ui/events`
|
|
11
|
+
* free of any registry coupling.
|
|
12
|
+
*
|
|
13
|
+
* The output objects are plain JSON-compatible records typed `unknown` at the
|
|
14
|
+
* boundary; the registry validates/narrows them with its own
|
|
15
|
+
* `validateActionGraph`. We do not import registry types here.
|
|
16
|
+
*/
|
|
17
|
+
import type { EthAction, EthEventBinding } from "./types.js";
|
|
18
|
+
/** A registry-shaped action node (discriminant `action`). */
|
|
19
|
+
export type RegistryActionNode = Record<string, unknown> & {
|
|
20
|
+
action: string;
|
|
21
|
+
};
|
|
22
|
+
/** A registry-shaped event binding (`{ event, actions: ActionNode[] }`). */
|
|
23
|
+
export interface RegistryEventBinding {
|
|
24
|
+
event: string;
|
|
25
|
+
actions: RegistryActionNode[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Convert one {@link EthAction} to the registry `ActionNode` shape. The mapping:
|
|
29
|
+
* - `kind` → `action`
|
|
30
|
+
* - `runProcess`: `payload` → `externalInput`, `target`/`expectedVersion`/`optimistic` pass through
|
|
31
|
+
* - `navigate`: `target` → `route`, `params` pass through
|
|
32
|
+
* - `setState`: `target` → `key`, `value` passes through
|
|
33
|
+
* - `openModal`/`closeModal`: `target` → `modalId`
|
|
34
|
+
* - `showToast`: `message`/`level` pass through
|
|
35
|
+
*/
|
|
36
|
+
export declare function toRegistryAction(action: EthAction): RegistryActionNode;
|
|
37
|
+
/**
|
|
38
|
+
* Convert an {@link EthEventBinding} to the registry `EventBinding` shape. A host
|
|
39
|
+
* that speaks the registry contract uses this to adapt at the dispatch edge:
|
|
40
|
+
*
|
|
41
|
+
* ```ts
|
|
42
|
+
* const dispatch: EchoEventDispatch = (binding, payload) => {
|
|
43
|
+
* const reg = toRegistryEventBinding(binding);
|
|
44
|
+
* runtime.dispatch([reg], binding.event, { row: payload });
|
|
45
|
+
* };
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare function toRegistryEventBinding(binding: EthEventBinding): RegistryEventBinding;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `EchoEventContext` — the injectable dispatch surface a component reads to send
|
|
3
|
+
* its authored {@link EthEventBinding} to the backend.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the registry's `EventDispatchContext` / `PageInteractionProvider`
|
|
6
|
+
* pattern: the UI library owns the CONTEXT + hooks; the HOST (the echothink-fugue
|
|
7
|
+
* renderer, a demo app, or a test) provides the real `dispatch` implementation
|
|
8
|
+
* that walks the action graph → `runProcess` → gateway. A NO-OP default lets
|
|
9
|
+
* components render with no provider, so standalone render/snapshot tests never
|
|
10
|
+
* crash.
|
|
11
|
+
*/
|
|
12
|
+
import { type ReactNode } from "react";
|
|
13
|
+
import type { EchoEventDispatch } from "./types.js";
|
|
14
|
+
/**
|
|
15
|
+
* The interaction surface a component reads via {@link useEchoEvents}. The host
|
|
16
|
+
* constructs the real one; absent a provider, {@link NOOP_DISPATCH} is used so
|
|
17
|
+
* components never crash.
|
|
18
|
+
*/
|
|
19
|
+
export interface EchoEventContextValue {
|
|
20
|
+
/** Carry a binding (+ live payload) to the backend. */
|
|
21
|
+
dispatch: EchoEventDispatch;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Provide an {@link EchoEventDispatch} (constructed by the host) to the
|
|
25
|
+
* component tree. The host's `dispatch` is the bridge to the backend — it
|
|
26
|
+
* typically adapts each {@link EthEventBinding} to the registry action graph and
|
|
27
|
+
* runs it through `runProcess` → gateway.
|
|
28
|
+
*
|
|
29
|
+
* ```tsx
|
|
30
|
+
* <EchoEventProvider dispatch={(binding, payload) => runtime.dispatch(binding, payload)}>
|
|
31
|
+
* <MyAuthoredPage />
|
|
32
|
+
* </EchoEventProvider>
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function EchoEventProvider({ dispatch, children, }: {
|
|
36
|
+
dispatch: EchoEventDispatch;
|
|
37
|
+
children: ReactNode;
|
|
38
|
+
}): ReactNode;
|
|
39
|
+
/**
|
|
40
|
+
* Read the {@link EchoEventContextValue}. Returns the no-op dispatch when no
|
|
41
|
+
* provider is mounted, so components (and their render tests) work standalone.
|
|
42
|
+
*/
|
|
43
|
+
export declare function useEchoEvents(): EchoEventContextValue;
|
package/dist/hooks.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { EthDispatchData, EthEventBinding } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Build an `onSelect` / `onClick`-shaped handler from an {@link EthEventBinding}.
|
|
4
|
+
*
|
|
5
|
+
* Returns `undefined` when `binding` is absent, so a component can spread it into
|
|
6
|
+
* `EthAction.onSelect` and fall back to its own default when unbound:
|
|
7
|
+
*
|
|
8
|
+
* ```tsx
|
|
9
|
+
* const onSelect = useEthAction(action.binding);
|
|
10
|
+
* <Button onClick={onSelect}>{action.label}</Button>
|
|
11
|
+
* // or, for EthAction:
|
|
12
|
+
* { ...action, onSelect: useEthAction(action.binding) ?? action.onSelect }
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* The optional `data` (current row / route params) is threaded into the dispatch
|
|
16
|
+
* so the host can resolve `field:` / `route:` payloads — matching the registry's
|
|
17
|
+
* `DispatchDataContext`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function useEthAction(binding: EthEventBinding | undefined, data?: EthDispatchData): (() => void) | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Build a payload-carrying handler from an {@link EthEventBinding} — for events
|
|
22
|
+
* that produce live data (form `onSubmit` values, the selected row, the chosen
|
|
23
|
+
* `rowAction` id). The returned function forwards whatever it is called with as
|
|
24
|
+
* the dispatch `payload`, so the host can merge it into the binding's actions.
|
|
25
|
+
*
|
|
26
|
+
* Returns a no-op (still callable) when `binding` is absent, so callers don't
|
|
27
|
+
* need to null-check at the call site.
|
|
28
|
+
*
|
|
29
|
+
* ```tsx
|
|
30
|
+
* const onSubmit = useEthEventHandler(binding);
|
|
31
|
+
* <form onSubmit={(e) => { e.preventDefault(); onSubmit(readFormValues(e)); }} />
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function useEthEventHandler<P = unknown>(binding: EthEventBinding | undefined, data?: EthDispatchData): (payload?: P) => void;
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
EchoEventProvider: () => EchoEventProvider,
|
|
24
|
+
toRegistryAction: () => toRegistryAction,
|
|
25
|
+
toRegistryEventBinding: () => toRegistryEventBinding,
|
|
26
|
+
useEchoEvents: () => useEchoEvents,
|
|
27
|
+
useEthAction: () => useEthAction,
|
|
28
|
+
useEthEventHandler: () => useEthEventHandler
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
|
+
|
|
32
|
+
// src/context.tsx
|
|
33
|
+
var import_react = require("react");
|
|
34
|
+
var NOOP_DISPATCH = () => {
|
|
35
|
+
};
|
|
36
|
+
var EchoEventContext = (0, import_react.createContext)({
|
|
37
|
+
dispatch: NOOP_DISPATCH
|
|
38
|
+
});
|
|
39
|
+
function EchoEventProvider({
|
|
40
|
+
dispatch,
|
|
41
|
+
children
|
|
42
|
+
}) {
|
|
43
|
+
return (0, import_react.createElement)(
|
|
44
|
+
EchoEventContext.Provider,
|
|
45
|
+
{ value: { dispatch } },
|
|
46
|
+
children
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
function useEchoEvents() {
|
|
50
|
+
return (0, import_react.useContext)(EchoEventContext);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/hooks.tsx
|
|
54
|
+
var import_react2 = require("react");
|
|
55
|
+
function useEthAction(binding, data) {
|
|
56
|
+
const { dispatch } = useEchoEvents();
|
|
57
|
+
return (0, import_react2.useCallback)(() => {
|
|
58
|
+
if (!binding) return;
|
|
59
|
+
void dispatch(binding, void 0, data);
|
|
60
|
+
}, [dispatch, binding, data]);
|
|
61
|
+
}
|
|
62
|
+
function useEthEventHandler(binding, data) {
|
|
63
|
+
const { dispatch } = useEchoEvents();
|
|
64
|
+
return (0, import_react2.useCallback)(
|
|
65
|
+
(payload) => {
|
|
66
|
+
if (!binding) return;
|
|
67
|
+
void dispatch(binding, payload, data);
|
|
68
|
+
},
|
|
69
|
+
[dispatch, binding, data]
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/bridge.ts
|
|
74
|
+
function toRegistryAction(action) {
|
|
75
|
+
switch (action.kind) {
|
|
76
|
+
case "runProcess":
|
|
77
|
+
return {
|
|
78
|
+
action: "runProcess",
|
|
79
|
+
process: action.process,
|
|
80
|
+
...action.target !== void 0 ? { target: action.target } : {},
|
|
81
|
+
...action.payload !== void 0 ? { externalInput: action.payload } : {},
|
|
82
|
+
...action.expectedVersion !== void 0 ? { expectedVersion: action.expectedVersion } : {},
|
|
83
|
+
...action.optimistic !== void 0 ? { optimistic: action.optimistic } : {}
|
|
84
|
+
};
|
|
85
|
+
case "navigate":
|
|
86
|
+
return {
|
|
87
|
+
action: "navigate",
|
|
88
|
+
route: action.target,
|
|
89
|
+
...action.params !== void 0 ? { params: action.params } : {}
|
|
90
|
+
};
|
|
91
|
+
case "setState":
|
|
92
|
+
return {
|
|
93
|
+
action: "setState",
|
|
94
|
+
key: action.target,
|
|
95
|
+
...action.value !== void 0 ? { value: action.value } : {}
|
|
96
|
+
};
|
|
97
|
+
case "openModal":
|
|
98
|
+
return { action: "openModal", modalId: action.target };
|
|
99
|
+
case "closeModal":
|
|
100
|
+
return { action: "closeModal", modalId: action.target };
|
|
101
|
+
case "showToast":
|
|
102
|
+
return {
|
|
103
|
+
action: "showToast",
|
|
104
|
+
message: action.message,
|
|
105
|
+
...action.level !== void 0 ? { level: action.level } : {}
|
|
106
|
+
};
|
|
107
|
+
default: {
|
|
108
|
+
const unknownAction = action;
|
|
109
|
+
return { ...action, action: unknownAction.kind };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function toRegistryEventBinding(binding) {
|
|
114
|
+
return {
|
|
115
|
+
event: binding.event,
|
|
116
|
+
actions: (binding.actions ?? []).map(toRegistryAction)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
120
|
+
0 && (module.exports = {
|
|
121
|
+
EchoEventProvider,
|
|
122
|
+
toRegistryAction,
|
|
123
|
+
toRegistryEventBinding,
|
|
124
|
+
useEchoEvents,
|
|
125
|
+
useEthAction,
|
|
126
|
+
useEthEventHandler
|
|
127
|
+
});
|
|
128
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/context.tsx","../src/hooks.tsx","../src/bridge.ts"],"sourcesContent":["/**\n * `@echothink-ui/events` — public API.\n *\n * The backend-event contract for `@echothink-ui` components: a serializable\n * {@link EthEventBinding} (structurally compatible with the echothink-fugue\n * registry `EventBinding`), an {@link EchoEventProvider} the host uses to inject\n * a backend `dispatch`, a {@link useEchoEvents} hook, and component-facing\n * {@link useEthAction} / {@link useEthEventHandler} helpers that wire rich\n * semantic callbacks (`EthAction.onSelect`, form `onSubmit`, table `rowActions`)\n * to that dispatch. The {@link toRegistryEventBinding} bridge adapts the\n * authoring shape to the registry action-graph shape at the host edge.\n */\nexport type {\n EthEventName,\n EthActionTarget,\n EthActionKind,\n EthAction,\n EthRunProcessAction,\n EthNavigateAction,\n EthSetStateAction,\n EthOpenModalAction,\n EthCloseModalAction,\n EthShowToastAction,\n EthEventBinding,\n EthDispatchData,\n EchoEventDispatch,\n} from \"./types.js\";\n\nexport {\n EchoEventProvider,\n useEchoEvents,\n type EchoEventContextValue,\n} from \"./context.js\";\n\nexport { useEthAction, useEthEventHandler } from \"./hooks.js\";\n\nexport {\n toRegistryAction,\n toRegistryEventBinding,\n type RegistryActionNode,\n type RegistryEventBinding,\n} from \"./bridge.js\";\n","/**\n * `EchoEventContext` — the injectable dispatch surface a component reads to send\n * its authored {@link EthEventBinding} to the backend.\n *\n * Mirrors the registry's `EventDispatchContext` / `PageInteractionProvider`\n * pattern: the UI library owns the CONTEXT + hooks; the HOST (the echothink-fugue\n * renderer, a demo app, or a test) provides the real `dispatch` implementation\n * that walks the action graph → `runProcess` → gateway. A NO-OP default lets\n * components render with no provider, so standalone render/snapshot tests never\n * crash.\n */\nimport {\n createContext,\n createElement,\n useContext,\n type ReactNode,\n} from \"react\";\nimport type { EchoEventDispatch } from \"./types.js\";\n\n/**\n * The interaction surface a component reads via {@link useEchoEvents}. The host\n * constructs the real one; absent a provider, {@link NOOP_DISPATCH} is used so\n * components never crash.\n */\nexport interface EchoEventContextValue {\n /** Carry a binding (+ live payload) to the backend. */\n dispatch: EchoEventDispatch;\n}\n\n/** A no-op dispatch — used when no provider is mounted. */\nconst NOOP_DISPATCH: EchoEventDispatch = () => {};\n\nconst EchoEventContext = createContext<EchoEventContextValue>({\n dispatch: NOOP_DISPATCH,\n});\n\n/**\n * Provide an {@link EchoEventDispatch} (constructed by the host) to the\n * component tree. The host's `dispatch` is the bridge to the backend — it\n * typically adapts each {@link EthEventBinding} to the registry action graph and\n * runs it through `runProcess` → gateway.\n *\n * ```tsx\n * <EchoEventProvider dispatch={(binding, payload) => runtime.dispatch(binding, payload)}>\n * <MyAuthoredPage />\n * </EchoEventProvider>\n * ```\n */\nexport function EchoEventProvider({\n dispatch,\n children,\n}: {\n dispatch: EchoEventDispatch;\n children: ReactNode;\n}): ReactNode {\n return createElement(\n EchoEventContext.Provider,\n { value: { dispatch } },\n children,\n );\n}\n\n/**\n * Read the {@link EchoEventContextValue}. Returns the no-op dispatch when no\n * provider is mounted, so components (and their render tests) work standalone.\n */\nexport function useEchoEvents(): EchoEventContextValue {\n return useContext(EchoEventContext);\n}\n","/**\n * Component-facing helpers that wire a component's rich semantic callbacks\n * (`EthAction.onSelect`, `onClick`, form `onSubmit`, table `rowActions`) to a\n * backend {@link EthEventBinding} via the injected {@link EchoEventDispatch}.\n *\n * These are the OPT-IN seam: a component that wants backend-aware behavior takes\n * an optional `binding?: EthEventBinding` prop and calls one of these helpers to\n * produce the handler it passes to its native callback prop. A component with no\n * binding behaves exactly as today (the no-op dispatch is inert).\n */\nimport { useCallback } from \"react\";\nimport { useEchoEvents } from \"./context.js\";\nimport type { EthDispatchData, EthEventBinding } from \"./types.js\";\n\n/**\n * Build an `onSelect` / `onClick`-shaped handler from an {@link EthEventBinding}.\n *\n * Returns `undefined` when `binding` is absent, so a component can spread it into\n * `EthAction.onSelect` and fall back to its own default when unbound:\n *\n * ```tsx\n * const onSelect = useEthAction(action.binding);\n * <Button onClick={onSelect}>{action.label}</Button>\n * // or, for EthAction:\n * { ...action, onSelect: useEthAction(action.binding) ?? action.onSelect }\n * ```\n *\n * The optional `data` (current row / route params) is threaded into the dispatch\n * so the host can resolve `field:` / `route:` payloads — matching the registry's\n * `DispatchDataContext`.\n */\nexport function useEthAction(\n binding: EthEventBinding | undefined,\n data?: EthDispatchData,\n): (() => void) | undefined {\n const { dispatch } = useEchoEvents();\n return useCallback(() => {\n if (!binding) return;\n void dispatch(binding, undefined, data);\n }, [dispatch, binding, data]);\n}\n\n/**\n * Build a payload-carrying handler from an {@link EthEventBinding} — for events\n * that produce live data (form `onSubmit` values, the selected row, the chosen\n * `rowAction` id). The returned function forwards whatever it is called with as\n * the dispatch `payload`, so the host can merge it into the binding's actions.\n *\n * Returns a no-op (still callable) when `binding` is absent, so callers don't\n * need to null-check at the call site.\n *\n * ```tsx\n * const onSubmit = useEthEventHandler(binding);\n * <form onSubmit={(e) => { e.preventDefault(); onSubmit(readFormValues(e)); }} />\n * ```\n */\nexport function useEthEventHandler<P = unknown>(\n binding: EthEventBinding | undefined,\n data?: EthDispatchData,\n): (payload?: P) => void {\n const { dispatch } = useEchoEvents();\n return useCallback(\n (payload?: P) => {\n if (!binding) return;\n void dispatch(binding, payload, data);\n },\n [dispatch, binding, data],\n );\n}\n","/**\n * `bridge` — the field-renaming adapter that turns an {@link EthEventBinding}\n * (the UI-library shape, discriminant `kind`) into the echothink-fugue registry\n * `EventBinding` shape (discriminant `action`) that\n * `runActionGraph`/`runProcess` consume.\n *\n * This lives in the UI library (not the registry) so the UI side OWNS its\n * authoring shape and the host's `dispatch` can be a thin `runActionGraph(\n * toRegistryActions(binding.actions), ctx)`. It is the documented, single point\n * where the two contracts meet — keeping the rest of `@echothink-ui/events`\n * free of any registry coupling.\n *\n * The output objects are plain JSON-compatible records typed `unknown` at the\n * boundary; the registry validates/narrows them with its own\n * `validateActionGraph`. We do not import registry types here.\n */\nimport type { EthAction, EthEventBinding } from \"./types.js\";\n\n/** A registry-shaped action node (discriminant `action`). */\nexport type RegistryActionNode = Record<string, unknown> & { action: string };\n\n/** A registry-shaped event binding (`{ event, actions: ActionNode[] }`). */\nexport interface RegistryEventBinding {\n event: string;\n actions: RegistryActionNode[];\n}\n\n/**\n * Convert one {@link EthAction} to the registry `ActionNode` shape. The mapping:\n * - `kind` → `action`\n * - `runProcess`: `payload` → `externalInput`, `target`/`expectedVersion`/`optimistic` pass through\n * - `navigate`: `target` → `route`, `params` pass through\n * - `setState`: `target` → `key`, `value` passes through\n * - `openModal`/`closeModal`: `target` → `modalId`\n * - `showToast`: `message`/`level` pass through\n */\nexport function toRegistryAction(action: EthAction): RegistryActionNode {\n switch (action.kind) {\n case \"runProcess\":\n return {\n action: \"runProcess\",\n process: action.process,\n ...(action.target !== undefined ? { target: action.target } : {}),\n ...(action.payload !== undefined ? { externalInput: action.payload } : {}),\n ...(action.expectedVersion !== undefined\n ? { expectedVersion: action.expectedVersion }\n : {}),\n ...(action.optimistic !== undefined ? { optimistic: action.optimistic } : {}),\n };\n case \"navigate\":\n return {\n action: \"navigate\",\n route: action.target,\n ...(action.params !== undefined ? { params: action.params } : {}),\n };\n case \"setState\":\n return {\n action: \"setState\",\n key: action.target,\n ...(action.value !== undefined ? { value: action.value } : {}),\n };\n case \"openModal\":\n return { action: \"openModal\", modalId: action.target };\n case \"closeModal\":\n return { action: \"closeModal\", modalId: action.target };\n case \"showToast\":\n return {\n action: \"showToast\",\n message: action.message,\n ...(action.level !== undefined ? { level: action.level } : {}),\n };\n default: {\n // Forward-compatible: pass an unknown kind through as its own action.\n const unknownAction = action as { kind: string };\n return { ...(action as Record<string, unknown>), action: unknownAction.kind };\n }\n }\n}\n\n/**\n * Convert an {@link EthEventBinding} to the registry `EventBinding` shape. A host\n * that speaks the registry contract uses this to adapt at the dispatch edge:\n *\n * ```ts\n * const dispatch: EchoEventDispatch = (binding, payload) => {\n * const reg = toRegistryEventBinding(binding);\n * runtime.dispatch([reg], binding.event, { row: payload });\n * };\n * ```\n */\nexport function toRegistryEventBinding(binding: EthEventBinding): RegistryEventBinding {\n return {\n event: binding.event,\n actions: (binding.actions ?? []).map(toRegistryAction),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACWA,mBAKO;AAcP,IAAM,gBAAmC,MAAM;AAAC;AAEhD,IAAM,uBAAmB,4BAAqC;AAAA,EAC5D,UAAU;AACZ,CAAC;AAcM,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AACF,GAGc;AACZ,aAAO;AAAA,IACL,iBAAiB;AAAA,IACjB,EAAE,OAAO,EAAE,SAAS,EAAE;AAAA,IACtB;AAAA,EACF;AACF;AAMO,SAAS,gBAAuC;AACrD,aAAO,yBAAW,gBAAgB;AACpC;;;AC1DA,IAAAA,gBAA4B;AAqBrB,SAAS,aACd,SACA,MAC0B;AAC1B,QAAM,EAAE,SAAS,IAAI,cAAc;AACnC,aAAO,2BAAY,MAAM;AACvB,QAAI,CAAC,QAAS;AACd,SAAK,SAAS,SAAS,QAAW,IAAI;AAAA,EACxC,GAAG,CAAC,UAAU,SAAS,IAAI,CAAC;AAC9B;AAgBO,SAAS,mBACd,SACA,MACuB;AACvB,QAAM,EAAE,SAAS,IAAI,cAAc;AACnC,aAAO;AAAA,IACL,CAAC,YAAgB;AACf,UAAI,CAAC,QAAS;AACd,WAAK,SAAS,SAAS,SAAS,IAAI;AAAA,IACtC;AAAA,IACA,CAAC,UAAU,SAAS,IAAI;AAAA,EAC1B;AACF;;;AChCO,SAAS,iBAAiB,QAAuC;AACtE,UAAQ,OAAO,MAAM;AAAA,IACnB,KAAK;AACH,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,SAAS,OAAO;AAAA,QAChB,GAAI,OAAO,WAAW,SAAY,EAAE,QAAQ,OAAO,OAAO,IAAI,CAAC;AAAA,QAC/D,GAAI,OAAO,YAAY,SAAY,EAAE,eAAe,OAAO,QAAQ,IAAI,CAAC;AAAA,QACxE,GAAI,OAAO,oBAAoB,SAC3B,EAAE,iBAAiB,OAAO,gBAAgB,IAC1C,CAAC;AAAA,QACL,GAAI,OAAO,eAAe,SAAY,EAAE,YAAY,OAAO,WAAW,IAAI,CAAC;AAAA,MAC7E;AAAA,IACF,KAAK;AACH,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,OAAO,OAAO;AAAA,QACd,GAAI,OAAO,WAAW,SAAY,EAAE,QAAQ,OAAO,OAAO,IAAI,CAAC;AAAA,MACjE;AAAA,IACF,KAAK;AACH,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,KAAK,OAAO;AAAA,QACZ,GAAI,OAAO,UAAU,SAAY,EAAE,OAAO,OAAO,MAAM,IAAI,CAAC;AAAA,MAC9D;AAAA,IACF,KAAK;AACH,aAAO,EAAE,QAAQ,aAAa,SAAS,OAAO,OAAO;AAAA,IACvD,KAAK;AACH,aAAO,EAAE,QAAQ,cAAc,SAAS,OAAO,OAAO;AAAA,IACxD,KAAK;AACH,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,SAAS,OAAO;AAAA,QAChB,GAAI,OAAO,UAAU,SAAY,EAAE,OAAO,OAAO,MAAM,IAAI,CAAC;AAAA,MAC9D;AAAA,IACF,SAAS;AAEP,YAAM,gBAAgB;AACtB,aAAO,EAAE,GAAI,QAAoC,QAAQ,cAAc,KAAK;AAAA,IAC9E;AAAA,EACF;AACF;AAaO,SAAS,uBAAuB,SAAgD;AACrF,SAAO;AAAA,IACL,OAAO,QAAQ;AAAA,IACf,UAAU,QAAQ,WAAW,CAAC,GAAG,IAAI,gBAAgB;AAAA,EACvD;AACF;","names":["import_react"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@echothink-ui/events` — public API.
|
|
3
|
+
*
|
|
4
|
+
* The backend-event contract for `@echothink-ui` components: a serializable
|
|
5
|
+
* {@link EthEventBinding} (structurally compatible with the echothink-fugue
|
|
6
|
+
* registry `EventBinding`), an {@link EchoEventProvider} the host uses to inject
|
|
7
|
+
* a backend `dispatch`, a {@link useEchoEvents} hook, and component-facing
|
|
8
|
+
* {@link useEthAction} / {@link useEthEventHandler} helpers that wire rich
|
|
9
|
+
* semantic callbacks (`EthAction.onSelect`, form `onSubmit`, table `rowActions`)
|
|
10
|
+
* to that dispatch. The {@link toRegistryEventBinding} bridge adapts the
|
|
11
|
+
* authoring shape to the registry action-graph shape at the host edge.
|
|
12
|
+
*/
|
|
13
|
+
export type { EthEventName, EthActionTarget, EthActionKind, EthAction, EthRunProcessAction, EthNavigateAction, EthSetStateAction, EthOpenModalAction, EthCloseModalAction, EthShowToastAction, EthEventBinding, EthDispatchData, EchoEventDispatch, } from "./types.js";
|
|
14
|
+
export { EchoEventProvider, useEchoEvents, type EchoEventContextValue, } from "./context.js";
|
|
15
|
+
export { useEthAction, useEthEventHandler } from "./hooks.js";
|
|
16
|
+
export { toRegistryAction, toRegistryEventBinding, type RegistryActionNode, type RegistryEventBinding, } from "./bridge.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// src/context.tsx
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
createElement,
|
|
5
|
+
useContext
|
|
6
|
+
} from "react";
|
|
7
|
+
var NOOP_DISPATCH = () => {
|
|
8
|
+
};
|
|
9
|
+
var EchoEventContext = createContext({
|
|
10
|
+
dispatch: NOOP_DISPATCH
|
|
11
|
+
});
|
|
12
|
+
function EchoEventProvider({
|
|
13
|
+
dispatch,
|
|
14
|
+
children
|
|
15
|
+
}) {
|
|
16
|
+
return createElement(
|
|
17
|
+
EchoEventContext.Provider,
|
|
18
|
+
{ value: { dispatch } },
|
|
19
|
+
children
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
function useEchoEvents() {
|
|
23
|
+
return useContext(EchoEventContext);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/hooks.tsx
|
|
27
|
+
import { useCallback } from "react";
|
|
28
|
+
function useEthAction(binding, data) {
|
|
29
|
+
const { dispatch } = useEchoEvents();
|
|
30
|
+
return useCallback(() => {
|
|
31
|
+
if (!binding) return;
|
|
32
|
+
void dispatch(binding, void 0, data);
|
|
33
|
+
}, [dispatch, binding, data]);
|
|
34
|
+
}
|
|
35
|
+
function useEthEventHandler(binding, data) {
|
|
36
|
+
const { dispatch } = useEchoEvents();
|
|
37
|
+
return useCallback(
|
|
38
|
+
(payload) => {
|
|
39
|
+
if (!binding) return;
|
|
40
|
+
void dispatch(binding, payload, data);
|
|
41
|
+
},
|
|
42
|
+
[dispatch, binding, data]
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/bridge.ts
|
|
47
|
+
function toRegistryAction(action) {
|
|
48
|
+
switch (action.kind) {
|
|
49
|
+
case "runProcess":
|
|
50
|
+
return {
|
|
51
|
+
action: "runProcess",
|
|
52
|
+
process: action.process,
|
|
53
|
+
...action.target !== void 0 ? { target: action.target } : {},
|
|
54
|
+
...action.payload !== void 0 ? { externalInput: action.payload } : {},
|
|
55
|
+
...action.expectedVersion !== void 0 ? { expectedVersion: action.expectedVersion } : {},
|
|
56
|
+
...action.optimistic !== void 0 ? { optimistic: action.optimistic } : {}
|
|
57
|
+
};
|
|
58
|
+
case "navigate":
|
|
59
|
+
return {
|
|
60
|
+
action: "navigate",
|
|
61
|
+
route: action.target,
|
|
62
|
+
...action.params !== void 0 ? { params: action.params } : {}
|
|
63
|
+
};
|
|
64
|
+
case "setState":
|
|
65
|
+
return {
|
|
66
|
+
action: "setState",
|
|
67
|
+
key: action.target,
|
|
68
|
+
...action.value !== void 0 ? { value: action.value } : {}
|
|
69
|
+
};
|
|
70
|
+
case "openModal":
|
|
71
|
+
return { action: "openModal", modalId: action.target };
|
|
72
|
+
case "closeModal":
|
|
73
|
+
return { action: "closeModal", modalId: action.target };
|
|
74
|
+
case "showToast":
|
|
75
|
+
return {
|
|
76
|
+
action: "showToast",
|
|
77
|
+
message: action.message,
|
|
78
|
+
...action.level !== void 0 ? { level: action.level } : {}
|
|
79
|
+
};
|
|
80
|
+
default: {
|
|
81
|
+
const unknownAction = action;
|
|
82
|
+
return { ...action, action: unknownAction.kind };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function toRegistryEventBinding(binding) {
|
|
87
|
+
return {
|
|
88
|
+
event: binding.event,
|
|
89
|
+
actions: (binding.actions ?? []).map(toRegistryAction)
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export {
|
|
93
|
+
EchoEventProvider,
|
|
94
|
+
toRegistryAction,
|
|
95
|
+
toRegistryEventBinding,
|
|
96
|
+
useEchoEvents,
|
|
97
|
+
useEthAction,
|
|
98
|
+
useEthEventHandler
|
|
99
|
+
};
|
|
100
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/context.tsx","../src/hooks.tsx","../src/bridge.ts"],"sourcesContent":["/**\n * `EchoEventContext` — the injectable dispatch surface a component reads to send\n * its authored {@link EthEventBinding} to the backend.\n *\n * Mirrors the registry's `EventDispatchContext` / `PageInteractionProvider`\n * pattern: the UI library owns the CONTEXT + hooks; the HOST (the echothink-fugue\n * renderer, a demo app, or a test) provides the real `dispatch` implementation\n * that walks the action graph → `runProcess` → gateway. A NO-OP default lets\n * components render with no provider, so standalone render/snapshot tests never\n * crash.\n */\nimport {\n createContext,\n createElement,\n useContext,\n type ReactNode,\n} from \"react\";\nimport type { EchoEventDispatch } from \"./types.js\";\n\n/**\n * The interaction surface a component reads via {@link useEchoEvents}. The host\n * constructs the real one; absent a provider, {@link NOOP_DISPATCH} is used so\n * components never crash.\n */\nexport interface EchoEventContextValue {\n /** Carry a binding (+ live payload) to the backend. */\n dispatch: EchoEventDispatch;\n}\n\n/** A no-op dispatch — used when no provider is mounted. */\nconst NOOP_DISPATCH: EchoEventDispatch = () => {};\n\nconst EchoEventContext = createContext<EchoEventContextValue>({\n dispatch: NOOP_DISPATCH,\n});\n\n/**\n * Provide an {@link EchoEventDispatch} (constructed by the host) to the\n * component tree. The host's `dispatch` is the bridge to the backend — it\n * typically adapts each {@link EthEventBinding} to the registry action graph and\n * runs it through `runProcess` → gateway.\n *\n * ```tsx\n * <EchoEventProvider dispatch={(binding, payload) => runtime.dispatch(binding, payload)}>\n * <MyAuthoredPage />\n * </EchoEventProvider>\n * ```\n */\nexport function EchoEventProvider({\n dispatch,\n children,\n}: {\n dispatch: EchoEventDispatch;\n children: ReactNode;\n}): ReactNode {\n return createElement(\n EchoEventContext.Provider,\n { value: { dispatch } },\n children,\n );\n}\n\n/**\n * Read the {@link EchoEventContextValue}. Returns the no-op dispatch when no\n * provider is mounted, so components (and their render tests) work standalone.\n */\nexport function useEchoEvents(): EchoEventContextValue {\n return useContext(EchoEventContext);\n}\n","/**\n * Component-facing helpers that wire a component's rich semantic callbacks\n * (`EthAction.onSelect`, `onClick`, form `onSubmit`, table `rowActions`) to a\n * backend {@link EthEventBinding} via the injected {@link EchoEventDispatch}.\n *\n * These are the OPT-IN seam: a component that wants backend-aware behavior takes\n * an optional `binding?: EthEventBinding` prop and calls one of these helpers to\n * produce the handler it passes to its native callback prop. A component with no\n * binding behaves exactly as today (the no-op dispatch is inert).\n */\nimport { useCallback } from \"react\";\nimport { useEchoEvents } from \"./context.js\";\nimport type { EthDispatchData, EthEventBinding } from \"./types.js\";\n\n/**\n * Build an `onSelect` / `onClick`-shaped handler from an {@link EthEventBinding}.\n *\n * Returns `undefined` when `binding` is absent, so a component can spread it into\n * `EthAction.onSelect` and fall back to its own default when unbound:\n *\n * ```tsx\n * const onSelect = useEthAction(action.binding);\n * <Button onClick={onSelect}>{action.label}</Button>\n * // or, for EthAction:\n * { ...action, onSelect: useEthAction(action.binding) ?? action.onSelect }\n * ```\n *\n * The optional `data` (current row / route params) is threaded into the dispatch\n * so the host can resolve `field:` / `route:` payloads — matching the registry's\n * `DispatchDataContext`.\n */\nexport function useEthAction(\n binding: EthEventBinding | undefined,\n data?: EthDispatchData,\n): (() => void) | undefined {\n const { dispatch } = useEchoEvents();\n return useCallback(() => {\n if (!binding) return;\n void dispatch(binding, undefined, data);\n }, [dispatch, binding, data]);\n}\n\n/**\n * Build a payload-carrying handler from an {@link EthEventBinding} — for events\n * that produce live data (form `onSubmit` values, the selected row, the chosen\n * `rowAction` id). The returned function forwards whatever it is called with as\n * the dispatch `payload`, so the host can merge it into the binding's actions.\n *\n * Returns a no-op (still callable) when `binding` is absent, so callers don't\n * need to null-check at the call site.\n *\n * ```tsx\n * const onSubmit = useEthEventHandler(binding);\n * <form onSubmit={(e) => { e.preventDefault(); onSubmit(readFormValues(e)); }} />\n * ```\n */\nexport function useEthEventHandler<P = unknown>(\n binding: EthEventBinding | undefined,\n data?: EthDispatchData,\n): (payload?: P) => void {\n const { dispatch } = useEchoEvents();\n return useCallback(\n (payload?: P) => {\n if (!binding) return;\n void dispatch(binding, payload, data);\n },\n [dispatch, binding, data],\n );\n}\n","/**\n * `bridge` — the field-renaming adapter that turns an {@link EthEventBinding}\n * (the UI-library shape, discriminant `kind`) into the echothink-fugue registry\n * `EventBinding` shape (discriminant `action`) that\n * `runActionGraph`/`runProcess` consume.\n *\n * This lives in the UI library (not the registry) so the UI side OWNS its\n * authoring shape and the host's `dispatch` can be a thin `runActionGraph(\n * toRegistryActions(binding.actions), ctx)`. It is the documented, single point\n * where the two contracts meet — keeping the rest of `@echothink-ui/events`\n * free of any registry coupling.\n *\n * The output objects are plain JSON-compatible records typed `unknown` at the\n * boundary; the registry validates/narrows them with its own\n * `validateActionGraph`. We do not import registry types here.\n */\nimport type { EthAction, EthEventBinding } from \"./types.js\";\n\n/** A registry-shaped action node (discriminant `action`). */\nexport type RegistryActionNode = Record<string, unknown> & { action: string };\n\n/** A registry-shaped event binding (`{ event, actions: ActionNode[] }`). */\nexport interface RegistryEventBinding {\n event: string;\n actions: RegistryActionNode[];\n}\n\n/**\n * Convert one {@link EthAction} to the registry `ActionNode` shape. The mapping:\n * - `kind` → `action`\n * - `runProcess`: `payload` → `externalInput`, `target`/`expectedVersion`/`optimistic` pass through\n * - `navigate`: `target` → `route`, `params` pass through\n * - `setState`: `target` → `key`, `value` passes through\n * - `openModal`/`closeModal`: `target` → `modalId`\n * - `showToast`: `message`/`level` pass through\n */\nexport function toRegistryAction(action: EthAction): RegistryActionNode {\n switch (action.kind) {\n case \"runProcess\":\n return {\n action: \"runProcess\",\n process: action.process,\n ...(action.target !== undefined ? { target: action.target } : {}),\n ...(action.payload !== undefined ? { externalInput: action.payload } : {}),\n ...(action.expectedVersion !== undefined\n ? { expectedVersion: action.expectedVersion }\n : {}),\n ...(action.optimistic !== undefined ? { optimistic: action.optimistic } : {}),\n };\n case \"navigate\":\n return {\n action: \"navigate\",\n route: action.target,\n ...(action.params !== undefined ? { params: action.params } : {}),\n };\n case \"setState\":\n return {\n action: \"setState\",\n key: action.target,\n ...(action.value !== undefined ? { value: action.value } : {}),\n };\n case \"openModal\":\n return { action: \"openModal\", modalId: action.target };\n case \"closeModal\":\n return { action: \"closeModal\", modalId: action.target };\n case \"showToast\":\n return {\n action: \"showToast\",\n message: action.message,\n ...(action.level !== undefined ? { level: action.level } : {}),\n };\n default: {\n // Forward-compatible: pass an unknown kind through as its own action.\n const unknownAction = action as { kind: string };\n return { ...(action as Record<string, unknown>), action: unknownAction.kind };\n }\n }\n}\n\n/**\n * Convert an {@link EthEventBinding} to the registry `EventBinding` shape. A host\n * that speaks the registry contract uses this to adapt at the dispatch edge:\n *\n * ```ts\n * const dispatch: EchoEventDispatch = (binding, payload) => {\n * const reg = toRegistryEventBinding(binding);\n * runtime.dispatch([reg], binding.event, { row: payload });\n * };\n * ```\n */\nexport function toRegistryEventBinding(binding: EthEventBinding): RegistryEventBinding {\n return {\n event: binding.event,\n actions: (binding.actions ?? []).map(toRegistryAction),\n };\n}\n"],"mappings":";AAWA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAcP,IAAM,gBAAmC,MAAM;AAAC;AAEhD,IAAM,mBAAmB,cAAqC;AAAA,EAC5D,UAAU;AACZ,CAAC;AAcM,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AACF,GAGc;AACZ,SAAO;AAAA,IACL,iBAAiB;AAAA,IACjB,EAAE,OAAO,EAAE,SAAS,EAAE;AAAA,IACtB;AAAA,EACF;AACF;AAMO,SAAS,gBAAuC;AACrD,SAAO,WAAW,gBAAgB;AACpC;;;AC1DA,SAAS,mBAAmB;AAqBrB,SAAS,aACd,SACA,MAC0B;AAC1B,QAAM,EAAE,SAAS,IAAI,cAAc;AACnC,SAAO,YAAY,MAAM;AACvB,QAAI,CAAC,QAAS;AACd,SAAK,SAAS,SAAS,QAAW,IAAI;AAAA,EACxC,GAAG,CAAC,UAAU,SAAS,IAAI,CAAC;AAC9B;AAgBO,SAAS,mBACd,SACA,MACuB;AACvB,QAAM,EAAE,SAAS,IAAI,cAAc;AACnC,SAAO;AAAA,IACL,CAAC,YAAgB;AACf,UAAI,CAAC,QAAS;AACd,WAAK,SAAS,SAAS,SAAS,IAAI;AAAA,IACtC;AAAA,IACA,CAAC,UAAU,SAAS,IAAI;AAAA,EAC1B;AACF;;;AChCO,SAAS,iBAAiB,QAAuC;AACtE,UAAQ,OAAO,MAAM;AAAA,IACnB,KAAK;AACH,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,SAAS,OAAO;AAAA,QAChB,GAAI,OAAO,WAAW,SAAY,EAAE,QAAQ,OAAO,OAAO,IAAI,CAAC;AAAA,QAC/D,GAAI,OAAO,YAAY,SAAY,EAAE,eAAe,OAAO,QAAQ,IAAI,CAAC;AAAA,QACxE,GAAI,OAAO,oBAAoB,SAC3B,EAAE,iBAAiB,OAAO,gBAAgB,IAC1C,CAAC;AAAA,QACL,GAAI,OAAO,eAAe,SAAY,EAAE,YAAY,OAAO,WAAW,IAAI,CAAC;AAAA,MAC7E;AAAA,IACF,KAAK;AACH,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,OAAO,OAAO;AAAA,QACd,GAAI,OAAO,WAAW,SAAY,EAAE,QAAQ,OAAO,OAAO,IAAI,CAAC;AAAA,MACjE;AAAA,IACF,KAAK;AACH,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,KAAK,OAAO;AAAA,QACZ,GAAI,OAAO,UAAU,SAAY,EAAE,OAAO,OAAO,MAAM,IAAI,CAAC;AAAA,MAC9D;AAAA,IACF,KAAK;AACH,aAAO,EAAE,QAAQ,aAAa,SAAS,OAAO,OAAO;AAAA,IACvD,KAAK;AACH,aAAO,EAAE,QAAQ,cAAc,SAAS,OAAO,OAAO;AAAA,IACxD,KAAK;AACH,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,SAAS,OAAO;AAAA,QAChB,GAAI,OAAO,UAAU,SAAY,EAAE,OAAO,OAAO,MAAM,IAAI,CAAC;AAAA,MAC9D;AAAA,IACF,SAAS;AAEP,YAAM,gBAAgB;AACtB,aAAO,EAAE,GAAI,QAAoC,QAAQ,cAAc,KAAK;AAAA,IAC9E;AAAA,EACF;AACF;AAaO,SAAS,uBAAuB,SAAgD;AACrF,SAAO;AAAA,IACL,OAAO,QAAQ;AAAA,IACf,UAAU,QAAQ,WAAW,CAAC,GAAG,IAAI,gBAAgB;AAAA,EACvD;AACF;","names":[]}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@echothink-ui/events` — the BACKEND-EVENT CONTRACT for `@echothink-ui`
|
|
3
|
+
* components.
|
|
4
|
+
*
|
|
5
|
+
* `@echothink-ui` components expose plain React callbacks (`onClick`,
|
|
6
|
+
* `onSelect`, `EthAction.onSelect`, form `onSubmit`, table `rowActions`) with no
|
|
7
|
+
* path to a backend process/action. This module defines the serializable
|
|
8
|
+
* data shape — {@link EthEventBinding} + {@link EthAction} — that lets a
|
|
9
|
+
* builder-authored page declare "when this UI event fires, run these backend
|
|
10
|
+
* actions", plus the {@link EchoEventDispatch} contract a HOST injects to
|
|
11
|
+
* actually carry those actions to a backend (ultimately `runProcess` → gateway).
|
|
12
|
+
*
|
|
13
|
+
* STRUCTURAL COMPATIBILITY (the load-bearing constraint): these types are kept
|
|
14
|
+
* field-for-field compatible with the echothink-fugue registry's `EventBinding`
|
|
15
|
+
* / `ActionNode` contract (see `echothink-component-registry/.../events.tsx` and
|
|
16
|
+
* `echothink-app-renderer/.../logic/graph.ts`). A plain `EthEventBinding[]`
|
|
17
|
+
* array produced here flows through that registry's `eventsAware` HOC →
|
|
18
|
+
* `PageInteractionRuntime.dispatch` → `runActionGraph` → `runProcess` WITHOUT
|
|
19
|
+
* conversion. We deliberately DO NOT import the registry types (that would
|
|
20
|
+
* invert the dependency direction — the registry/renderer depend on the UI
|
|
21
|
+
* library, never the reverse); we mirror the shapes locally instead.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* The known UI event names. Kept open (`string & {}`) so custom / semantic
|
|
25
|
+
* events (e.g. `"onRowAction:archive"`) flow through, mirroring the registry's
|
|
26
|
+
* `EventName`.
|
|
27
|
+
*/
|
|
28
|
+
export type EthEventName = "onClick" | "onSelect" | "onSubmit" | "onChange" | "onLoad" | (string & {});
|
|
29
|
+
/** Optimistic-process target reference (the entity the process acts on). */
|
|
30
|
+
export interface EthActionTarget {
|
|
31
|
+
entityType?: string;
|
|
32
|
+
entityId?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The action discriminant — the verbs the host runtime knows how to perform.
|
|
36
|
+
* Kept identical to the registry `ActionNode["action"]` vocabulary (the subset a
|
|
37
|
+
* UI component realistically authors); unknown kinds are tolerated by the
|
|
38
|
+
* runtime, so this stays forward-compatible.
|
|
39
|
+
*/
|
|
40
|
+
export type EthActionKind = "runProcess" | "navigate" | "setState" | "openModal" | "closeModal" | "showToast";
|
|
41
|
+
/** Invoke a unit process through the host runtime (ultimately the gateway). */
|
|
42
|
+
export interface EthRunProcessAction {
|
|
43
|
+
kind: "runProcess";
|
|
44
|
+
/** The unit-process name to invoke. */
|
|
45
|
+
process: string;
|
|
46
|
+
target?: EthActionTarget;
|
|
47
|
+
/** Inline payload for the process. May contain `field:` / `route:` refs. */
|
|
48
|
+
payload?: Record<string, unknown>;
|
|
49
|
+
expectedVersion?: number;
|
|
50
|
+
/** Hint that the UI applied an optimistic change before this ran. */
|
|
51
|
+
optimistic?: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** Client-side navigation to a route, with optional params. */
|
|
54
|
+
export interface EthNavigateAction {
|
|
55
|
+
kind: "navigate";
|
|
56
|
+
/** Target route / href. */
|
|
57
|
+
target: string;
|
|
58
|
+
params?: Record<string, string>;
|
|
59
|
+
}
|
|
60
|
+
/** Set a page-scoped state key to a value. */
|
|
61
|
+
export interface EthSetStateAction {
|
|
62
|
+
kind: "setState";
|
|
63
|
+
/** State key to set. */
|
|
64
|
+
target: string;
|
|
65
|
+
value?: unknown;
|
|
66
|
+
}
|
|
67
|
+
/** Open a modal by id. */
|
|
68
|
+
export interface EthOpenModalAction {
|
|
69
|
+
kind: "openModal";
|
|
70
|
+
/** Modal id to open. */
|
|
71
|
+
target: string;
|
|
72
|
+
}
|
|
73
|
+
/** Close a modal by id. */
|
|
74
|
+
export interface EthCloseModalAction {
|
|
75
|
+
kind: "closeModal";
|
|
76
|
+
/** Modal id to close. */
|
|
77
|
+
target: string;
|
|
78
|
+
}
|
|
79
|
+
/** Surface a transient toast notification. */
|
|
80
|
+
export interface EthShowToastAction {
|
|
81
|
+
kind: "showToast";
|
|
82
|
+
/** Toast message (the `target` mirrors `message` for field uniformity). */
|
|
83
|
+
message: string;
|
|
84
|
+
level?: "info" | "success" | "error";
|
|
85
|
+
}
|
|
86
|
+
/** The action union an {@link EthEventBinding} carries (discriminant: `kind`). */
|
|
87
|
+
export type EthAction = EthRunProcessAction | EthNavigateAction | EthSetStateAction | EthOpenModalAction | EthCloseModalAction | EthShowToastAction;
|
|
88
|
+
/**
|
|
89
|
+
* An event → action(s) binding attached to a component (the value a builder
|
|
90
|
+
* authors). When the named `event` fires the host runs `actions` in order.
|
|
91
|
+
*
|
|
92
|
+
* Structurally compatible with the registry `EventBinding` — see the conversion
|
|
93
|
+
* note in {@link toRegistryEventBinding}: `event` is the same field; the
|
|
94
|
+
* registry calls its action list `actions` with an `action` discriminant, while
|
|
95
|
+
* we use `kind` for ergonomics. {@link toRegistryEventBinding} renames the two
|
|
96
|
+
* fields (`kind`→`action`, `payload`→`externalInput`, `target`→`route`/`modalId`/`key`)
|
|
97
|
+
* so a host that speaks the registry contract can adapt at the edge.
|
|
98
|
+
*/
|
|
99
|
+
export interface EthEventBinding {
|
|
100
|
+
/** The UI event this binding reacts to. */
|
|
101
|
+
event: EthEventName;
|
|
102
|
+
/** The ordered backend/client actions to run when `event` fires. */
|
|
103
|
+
actions: EthAction[];
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Optional dispatch-time data context, structurally compatible with the
|
|
107
|
+
* registry's `DispatchDataContext` / `EventDispatchData` — `row` is the current
|
|
108
|
+
* data-binding row (for `field:` resolution), `route` the route params.
|
|
109
|
+
*/
|
|
110
|
+
export interface EthDispatchData {
|
|
111
|
+
row?: Record<string, unknown>;
|
|
112
|
+
route?: Record<string, string>;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The capability a HOST injects (via {@link EchoEventProvider}) to carry a
|
|
116
|
+
* component's authored binding to the backend. `payload` is the live runtime
|
|
117
|
+
* data captured at the event (e.g. the form values, the selected row, the
|
|
118
|
+
* chosen action id). The host is responsible for merging `payload` into the
|
|
119
|
+
* binding's actions and running them (typically by adapting to the registry
|
|
120
|
+
* action graph → `runProcess` → gateway).
|
|
121
|
+
*
|
|
122
|
+
* Returns the host's promise so callers MAY await completion (e.g. to clear an
|
|
123
|
+
* optimistic flag); a fire-and-forget host returns `void`.
|
|
124
|
+
*/
|
|
125
|
+
export type EchoEventDispatch = (binding: EthEventBinding, payload?: unknown, data?: EthDispatchData) => void | Promise<void>;
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@echothink-ui/events",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"react": ">=18.3.0",
|
|
23
|
+
"react-dom": ">=18.3.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^24.9.1",
|
|
27
|
+
"@types/react": "^19.2.2",
|
|
28
|
+
"@types/react-dom": "^19.2.2"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsup src/index.ts --format esm,cjs --sourcemap --clean --external react --external react-dom && tsc -p tsconfig.json --declaration --emitDeclarationOnly --noEmit false --outDir dist",
|
|
35
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
36
|
+
"test": "vitest run --root . --config ../echothink-UI-components/vitest.config.ts --passWithNoTests",
|
|
37
|
+
"lint": "eslint src"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
|
|
6
|
+
// Opt into React's act() testing environment so state updates flush synchronously
|
|
7
|
+
// and React does not warn about an unconfigured act environment.
|
|
8
|
+
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
EchoEventProvider,
|
|
12
|
+
useEthAction,
|
|
13
|
+
useEthEventHandler,
|
|
14
|
+
toRegistryEventBinding,
|
|
15
|
+
type EchoEventDispatch,
|
|
16
|
+
type EthEventBinding,
|
|
17
|
+
} from "../index.js";
|
|
18
|
+
|
|
19
|
+
/* A binding a builder would author for a Button → backend process. */
|
|
20
|
+
const archiveBinding: EthEventBinding = {
|
|
21
|
+
event: "onSelect",
|
|
22
|
+
actions: [
|
|
23
|
+
{ kind: "runProcess", process: "ArchiveOrder", payload: { reason: "stale" } },
|
|
24
|
+
{ kind: "showToast", message: "Archived", level: "success" },
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Mount a component into a real DOM container (jsdom) and return cleanup. */
|
|
29
|
+
function mount(ui: React.ReactElement) {
|
|
30
|
+
const container = document.createElement("div");
|
|
31
|
+
document.body.appendChild(container);
|
|
32
|
+
const root = createRoot(container);
|
|
33
|
+
act(() => {
|
|
34
|
+
root.render(ui);
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
container,
|
|
38
|
+
cleanup() {
|
|
39
|
+
act(() => root.unmount());
|
|
40
|
+
container.remove();
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* A tiny component that wires EthAction.onSelect → useEthAction(binding). */
|
|
46
|
+
function ArchiveButton({ binding }: { binding?: EthEventBinding }) {
|
|
47
|
+
const onSelect = useEthAction(binding);
|
|
48
|
+
return (
|
|
49
|
+
<button type="button" data-testid="archive" onClick={onSelect}>
|
|
50
|
+
Archive
|
|
51
|
+
</button>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* A component that forwards live payload (form values) through the handler. */
|
|
56
|
+
function SubmitForm({ binding }: { binding?: EthEventBinding }) {
|
|
57
|
+
const onSubmit = useEthEventHandler<Record<string, unknown>>(binding);
|
|
58
|
+
return (
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
data-testid="submit"
|
|
62
|
+
onClick={() => onSubmit({ name: "Ada" })}
|
|
63
|
+
>
|
|
64
|
+
Submit
|
|
65
|
+
</button>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe("useEthAction → EchoEventProvider.dispatch", () => {
|
|
70
|
+
it("dispatches the exact binding when EthAction.onSelect fires", () => {
|
|
71
|
+
const dispatch = vi.fn<EchoEventDispatch>();
|
|
72
|
+
const { container, cleanup } = mount(
|
|
73
|
+
<EchoEventProvider dispatch={dispatch}>
|
|
74
|
+
<ArchiveButton binding={archiveBinding} />
|
|
75
|
+
</EchoEventProvider>,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const button = container.querySelector<HTMLButtonElement>('[data-testid="archive"]')!;
|
|
79
|
+
act(() => button.click());
|
|
80
|
+
|
|
81
|
+
expect(dispatch).toHaveBeenCalledTimes(1);
|
|
82
|
+
expect(dispatch).toHaveBeenCalledWith(archiveBinding, undefined, undefined);
|
|
83
|
+
cleanup();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("is inert (no dispatch) when no binding is authored", () => {
|
|
87
|
+
const dispatch = vi.fn<EchoEventDispatch>();
|
|
88
|
+
const { container, cleanup } = mount(
|
|
89
|
+
<EchoEventProvider dispatch={dispatch}>
|
|
90
|
+
<ArchiveButton />
|
|
91
|
+
</EchoEventProvider>,
|
|
92
|
+
);
|
|
93
|
+
const button = container.querySelector<HTMLButtonElement>('[data-testid="archive"]')!;
|
|
94
|
+
act(() => button.click());
|
|
95
|
+
expect(dispatch).not.toHaveBeenCalled();
|
|
96
|
+
cleanup();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("does not crash with no provider mounted (no-op dispatch)", () => {
|
|
100
|
+
const { container, cleanup } = mount(<ArchiveButton binding={archiveBinding} />);
|
|
101
|
+
const button = container.querySelector<HTMLButtonElement>('[data-testid="archive"]')!;
|
|
102
|
+
expect(() => act(() => button.click())).not.toThrow();
|
|
103
|
+
cleanup();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("useEthEventHandler — live payload", () => {
|
|
108
|
+
it("forwards the live payload to dispatch", () => {
|
|
109
|
+
const dispatch = vi.fn<EchoEventDispatch>();
|
|
110
|
+
const binding: EthEventBinding = {
|
|
111
|
+
event: "onSubmit",
|
|
112
|
+
actions: [{ kind: "runProcess", process: "CreateUser" }],
|
|
113
|
+
};
|
|
114
|
+
const { container, cleanup } = mount(
|
|
115
|
+
<EchoEventProvider dispatch={dispatch}>
|
|
116
|
+
<SubmitForm binding={binding} />
|
|
117
|
+
</EchoEventProvider>,
|
|
118
|
+
);
|
|
119
|
+
const button = container.querySelector<HTMLButtonElement>('[data-testid="submit"]')!;
|
|
120
|
+
act(() => button.click());
|
|
121
|
+
expect(dispatch).toHaveBeenCalledWith(binding, { name: "Ada" }, undefined);
|
|
122
|
+
cleanup();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("toRegistryEventBinding — registry structural compatibility", () => {
|
|
127
|
+
it("renames kind→action and payload→externalInput for runProcess", () => {
|
|
128
|
+
const reg = toRegistryEventBinding(archiveBinding);
|
|
129
|
+
expect(reg.event).toBe("onSelect");
|
|
130
|
+
expect(reg.actions[0]).toEqual({
|
|
131
|
+
action: "runProcess",
|
|
132
|
+
process: "ArchiveOrder",
|
|
133
|
+
externalInput: { reason: "stale" },
|
|
134
|
+
});
|
|
135
|
+
expect(reg.actions[1]).toEqual({
|
|
136
|
+
action: "showToast",
|
|
137
|
+
message: "Archived",
|
|
138
|
+
level: "success",
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("maps navigate/setState/openModal targets to registry field names", () => {
|
|
143
|
+
const reg = toRegistryEventBinding({
|
|
144
|
+
event: "onClick",
|
|
145
|
+
actions: [
|
|
146
|
+
{ kind: "navigate", target: "/orders", params: { tab: "open" } },
|
|
147
|
+
{ kind: "setState", target: "selected", value: 3 },
|
|
148
|
+
{ kind: "openModal", target: "confirm" },
|
|
149
|
+
],
|
|
150
|
+
});
|
|
151
|
+
expect(reg.actions[0]).toEqual({
|
|
152
|
+
action: "navigate",
|
|
153
|
+
route: "/orders",
|
|
154
|
+
params: { tab: "open" },
|
|
155
|
+
});
|
|
156
|
+
expect(reg.actions[1]).toEqual({ action: "setState", key: "selected", value: 3 });
|
|
157
|
+
expect(reg.actions[2]).toEqual({ action: "openModal", modalId: "confirm" });
|
|
158
|
+
});
|
|
159
|
+
});
|
package/src/bridge.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bridge` — the field-renaming adapter that turns an {@link EthEventBinding}
|
|
3
|
+
* (the UI-library shape, discriminant `kind`) into the echothink-fugue registry
|
|
4
|
+
* `EventBinding` shape (discriminant `action`) that
|
|
5
|
+
* `runActionGraph`/`runProcess` consume.
|
|
6
|
+
*
|
|
7
|
+
* This lives in the UI library (not the registry) so the UI side OWNS its
|
|
8
|
+
* authoring shape and the host's `dispatch` can be a thin `runActionGraph(
|
|
9
|
+
* toRegistryActions(binding.actions), ctx)`. It is the documented, single point
|
|
10
|
+
* where the two contracts meet — keeping the rest of `@echothink-ui/events`
|
|
11
|
+
* free of any registry coupling.
|
|
12
|
+
*
|
|
13
|
+
* The output objects are plain JSON-compatible records typed `unknown` at the
|
|
14
|
+
* boundary; the registry validates/narrows them with its own
|
|
15
|
+
* `validateActionGraph`. We do not import registry types here.
|
|
16
|
+
*/
|
|
17
|
+
import type { EthAction, EthEventBinding } from "./types.js";
|
|
18
|
+
|
|
19
|
+
/** A registry-shaped action node (discriminant `action`). */
|
|
20
|
+
export type RegistryActionNode = Record<string, unknown> & { action: string };
|
|
21
|
+
|
|
22
|
+
/** A registry-shaped event binding (`{ event, actions: ActionNode[] }`). */
|
|
23
|
+
export interface RegistryEventBinding {
|
|
24
|
+
event: string;
|
|
25
|
+
actions: RegistryActionNode[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Convert one {@link EthAction} to the registry `ActionNode` shape. The mapping:
|
|
30
|
+
* - `kind` → `action`
|
|
31
|
+
* - `runProcess`: `payload` → `externalInput`, `target`/`expectedVersion`/`optimistic` pass through
|
|
32
|
+
* - `navigate`: `target` → `route`, `params` pass through
|
|
33
|
+
* - `setState`: `target` → `key`, `value` passes through
|
|
34
|
+
* - `openModal`/`closeModal`: `target` → `modalId`
|
|
35
|
+
* - `showToast`: `message`/`level` pass through
|
|
36
|
+
*/
|
|
37
|
+
export function toRegistryAction(action: EthAction): RegistryActionNode {
|
|
38
|
+
switch (action.kind) {
|
|
39
|
+
case "runProcess":
|
|
40
|
+
return {
|
|
41
|
+
action: "runProcess",
|
|
42
|
+
process: action.process,
|
|
43
|
+
...(action.target !== undefined ? { target: action.target } : {}),
|
|
44
|
+
...(action.payload !== undefined ? { externalInput: action.payload } : {}),
|
|
45
|
+
...(action.expectedVersion !== undefined
|
|
46
|
+
? { expectedVersion: action.expectedVersion }
|
|
47
|
+
: {}),
|
|
48
|
+
...(action.optimistic !== undefined ? { optimistic: action.optimistic } : {}),
|
|
49
|
+
};
|
|
50
|
+
case "navigate":
|
|
51
|
+
return {
|
|
52
|
+
action: "navigate",
|
|
53
|
+
route: action.target,
|
|
54
|
+
...(action.params !== undefined ? { params: action.params } : {}),
|
|
55
|
+
};
|
|
56
|
+
case "setState":
|
|
57
|
+
return {
|
|
58
|
+
action: "setState",
|
|
59
|
+
key: action.target,
|
|
60
|
+
...(action.value !== undefined ? { value: action.value } : {}),
|
|
61
|
+
};
|
|
62
|
+
case "openModal":
|
|
63
|
+
return { action: "openModal", modalId: action.target };
|
|
64
|
+
case "closeModal":
|
|
65
|
+
return { action: "closeModal", modalId: action.target };
|
|
66
|
+
case "showToast":
|
|
67
|
+
return {
|
|
68
|
+
action: "showToast",
|
|
69
|
+
message: action.message,
|
|
70
|
+
...(action.level !== undefined ? { level: action.level } : {}),
|
|
71
|
+
};
|
|
72
|
+
default: {
|
|
73
|
+
// Forward-compatible: pass an unknown kind through as its own action.
|
|
74
|
+
const unknownAction = action as { kind: string };
|
|
75
|
+
return { ...(action as Record<string, unknown>), action: unknownAction.kind };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Convert an {@link EthEventBinding} to the registry `EventBinding` shape. A host
|
|
82
|
+
* that speaks the registry contract uses this to adapt at the dispatch edge:
|
|
83
|
+
*
|
|
84
|
+
* ```ts
|
|
85
|
+
* const dispatch: EchoEventDispatch = (binding, payload) => {
|
|
86
|
+
* const reg = toRegistryEventBinding(binding);
|
|
87
|
+
* runtime.dispatch([reg], binding.event, { row: payload });
|
|
88
|
+
* };
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export function toRegistryEventBinding(binding: EthEventBinding): RegistryEventBinding {
|
|
92
|
+
return {
|
|
93
|
+
event: binding.event,
|
|
94
|
+
actions: (binding.actions ?? []).map(toRegistryAction),
|
|
95
|
+
};
|
|
96
|
+
}
|
package/src/context.tsx
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `EchoEventContext` — the injectable dispatch surface a component reads to send
|
|
3
|
+
* its authored {@link EthEventBinding} to the backend.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the registry's `EventDispatchContext` / `PageInteractionProvider`
|
|
6
|
+
* pattern: the UI library owns the CONTEXT + hooks; the HOST (the echothink-fugue
|
|
7
|
+
* renderer, a demo app, or a test) provides the real `dispatch` implementation
|
|
8
|
+
* that walks the action graph → `runProcess` → gateway. A NO-OP default lets
|
|
9
|
+
* components render with no provider, so standalone render/snapshot tests never
|
|
10
|
+
* crash.
|
|
11
|
+
*/
|
|
12
|
+
import {
|
|
13
|
+
createContext,
|
|
14
|
+
createElement,
|
|
15
|
+
useContext,
|
|
16
|
+
type ReactNode,
|
|
17
|
+
} from "react";
|
|
18
|
+
import type { EchoEventDispatch } from "./types.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The interaction surface a component reads via {@link useEchoEvents}. The host
|
|
22
|
+
* constructs the real one; absent a provider, {@link NOOP_DISPATCH} is used so
|
|
23
|
+
* components never crash.
|
|
24
|
+
*/
|
|
25
|
+
export interface EchoEventContextValue {
|
|
26
|
+
/** Carry a binding (+ live payload) to the backend. */
|
|
27
|
+
dispatch: EchoEventDispatch;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A no-op dispatch — used when no provider is mounted. */
|
|
31
|
+
const NOOP_DISPATCH: EchoEventDispatch = () => {};
|
|
32
|
+
|
|
33
|
+
const EchoEventContext = createContext<EchoEventContextValue>({
|
|
34
|
+
dispatch: NOOP_DISPATCH,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Provide an {@link EchoEventDispatch} (constructed by the host) to the
|
|
39
|
+
* component tree. The host's `dispatch` is the bridge to the backend — it
|
|
40
|
+
* typically adapts each {@link EthEventBinding} to the registry action graph and
|
|
41
|
+
* runs it through `runProcess` → gateway.
|
|
42
|
+
*
|
|
43
|
+
* ```tsx
|
|
44
|
+
* <EchoEventProvider dispatch={(binding, payload) => runtime.dispatch(binding, payload)}>
|
|
45
|
+
* <MyAuthoredPage />
|
|
46
|
+
* </EchoEventProvider>
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function EchoEventProvider({
|
|
50
|
+
dispatch,
|
|
51
|
+
children,
|
|
52
|
+
}: {
|
|
53
|
+
dispatch: EchoEventDispatch;
|
|
54
|
+
children: ReactNode;
|
|
55
|
+
}): ReactNode {
|
|
56
|
+
return createElement(
|
|
57
|
+
EchoEventContext.Provider,
|
|
58
|
+
{ value: { dispatch } },
|
|
59
|
+
children,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Read the {@link EchoEventContextValue}. Returns the no-op dispatch when no
|
|
65
|
+
* provider is mounted, so components (and their render tests) work standalone.
|
|
66
|
+
*/
|
|
67
|
+
export function useEchoEvents(): EchoEventContextValue {
|
|
68
|
+
return useContext(EchoEventContext);
|
|
69
|
+
}
|
package/src/hooks.tsx
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component-facing helpers that wire a component's rich semantic callbacks
|
|
3
|
+
* (`EthAction.onSelect`, `onClick`, form `onSubmit`, table `rowActions`) to a
|
|
4
|
+
* backend {@link EthEventBinding} via the injected {@link EchoEventDispatch}.
|
|
5
|
+
*
|
|
6
|
+
* These are the OPT-IN seam: a component that wants backend-aware behavior takes
|
|
7
|
+
* an optional `binding?: EthEventBinding` prop and calls one of these helpers to
|
|
8
|
+
* produce the handler it passes to its native callback prop. A component with no
|
|
9
|
+
* binding behaves exactly as today (the no-op dispatch is inert).
|
|
10
|
+
*/
|
|
11
|
+
import { useCallback } from "react";
|
|
12
|
+
import { useEchoEvents } from "./context.js";
|
|
13
|
+
import type { EthDispatchData, EthEventBinding } from "./types.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Build an `onSelect` / `onClick`-shaped handler from an {@link EthEventBinding}.
|
|
17
|
+
*
|
|
18
|
+
* Returns `undefined` when `binding` is absent, so a component can spread it into
|
|
19
|
+
* `EthAction.onSelect` and fall back to its own default when unbound:
|
|
20
|
+
*
|
|
21
|
+
* ```tsx
|
|
22
|
+
* const onSelect = useEthAction(action.binding);
|
|
23
|
+
* <Button onClick={onSelect}>{action.label}</Button>
|
|
24
|
+
* // or, for EthAction:
|
|
25
|
+
* { ...action, onSelect: useEthAction(action.binding) ?? action.onSelect }
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* The optional `data` (current row / route params) is threaded into the dispatch
|
|
29
|
+
* so the host can resolve `field:` / `route:` payloads — matching the registry's
|
|
30
|
+
* `DispatchDataContext`.
|
|
31
|
+
*/
|
|
32
|
+
export function useEthAction(
|
|
33
|
+
binding: EthEventBinding | undefined,
|
|
34
|
+
data?: EthDispatchData,
|
|
35
|
+
): (() => void) | undefined {
|
|
36
|
+
const { dispatch } = useEchoEvents();
|
|
37
|
+
return useCallback(() => {
|
|
38
|
+
if (!binding) return;
|
|
39
|
+
void dispatch(binding, undefined, data);
|
|
40
|
+
}, [dispatch, binding, data]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build a payload-carrying handler from an {@link EthEventBinding} — for events
|
|
45
|
+
* that produce live data (form `onSubmit` values, the selected row, the chosen
|
|
46
|
+
* `rowAction` id). The returned function forwards whatever it is called with as
|
|
47
|
+
* the dispatch `payload`, so the host can merge it into the binding's actions.
|
|
48
|
+
*
|
|
49
|
+
* Returns a no-op (still callable) when `binding` is absent, so callers don't
|
|
50
|
+
* need to null-check at the call site.
|
|
51
|
+
*
|
|
52
|
+
* ```tsx
|
|
53
|
+
* const onSubmit = useEthEventHandler(binding);
|
|
54
|
+
* <form onSubmit={(e) => { e.preventDefault(); onSubmit(readFormValues(e)); }} />
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function useEthEventHandler<P = unknown>(
|
|
58
|
+
binding: EthEventBinding | undefined,
|
|
59
|
+
data?: EthDispatchData,
|
|
60
|
+
): (payload?: P) => void {
|
|
61
|
+
const { dispatch } = useEchoEvents();
|
|
62
|
+
return useCallback(
|
|
63
|
+
(payload?: P) => {
|
|
64
|
+
if (!binding) return;
|
|
65
|
+
void dispatch(binding, payload, data);
|
|
66
|
+
},
|
|
67
|
+
[dispatch, binding, data],
|
|
68
|
+
);
|
|
69
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@echothink-ui/events` — public API.
|
|
3
|
+
*
|
|
4
|
+
* The backend-event contract for `@echothink-ui` components: a serializable
|
|
5
|
+
* {@link EthEventBinding} (structurally compatible with the echothink-fugue
|
|
6
|
+
* registry `EventBinding`), an {@link EchoEventProvider} the host uses to inject
|
|
7
|
+
* a backend `dispatch`, a {@link useEchoEvents} hook, and component-facing
|
|
8
|
+
* {@link useEthAction} / {@link useEthEventHandler} helpers that wire rich
|
|
9
|
+
* semantic callbacks (`EthAction.onSelect`, form `onSubmit`, table `rowActions`)
|
|
10
|
+
* to that dispatch. The {@link toRegistryEventBinding} bridge adapts the
|
|
11
|
+
* authoring shape to the registry action-graph shape at the host edge.
|
|
12
|
+
*/
|
|
13
|
+
export type {
|
|
14
|
+
EthEventName,
|
|
15
|
+
EthActionTarget,
|
|
16
|
+
EthActionKind,
|
|
17
|
+
EthAction,
|
|
18
|
+
EthRunProcessAction,
|
|
19
|
+
EthNavigateAction,
|
|
20
|
+
EthSetStateAction,
|
|
21
|
+
EthOpenModalAction,
|
|
22
|
+
EthCloseModalAction,
|
|
23
|
+
EthShowToastAction,
|
|
24
|
+
EthEventBinding,
|
|
25
|
+
EthDispatchData,
|
|
26
|
+
EchoEventDispatch,
|
|
27
|
+
} from "./types.js";
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
EchoEventProvider,
|
|
31
|
+
useEchoEvents,
|
|
32
|
+
type EchoEventContextValue,
|
|
33
|
+
} from "./context.js";
|
|
34
|
+
|
|
35
|
+
export { useEthAction, useEthEventHandler } from "./hooks.js";
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
toRegistryAction,
|
|
39
|
+
toRegistryEventBinding,
|
|
40
|
+
type RegistryActionNode,
|
|
41
|
+
type RegistryEventBinding,
|
|
42
|
+
} from "./bridge.js";
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@echothink-ui/events` — the BACKEND-EVENT CONTRACT for `@echothink-ui`
|
|
3
|
+
* components.
|
|
4
|
+
*
|
|
5
|
+
* `@echothink-ui` components expose plain React callbacks (`onClick`,
|
|
6
|
+
* `onSelect`, `EthAction.onSelect`, form `onSubmit`, table `rowActions`) with no
|
|
7
|
+
* path to a backend process/action. This module defines the serializable
|
|
8
|
+
* data shape — {@link EthEventBinding} + {@link EthAction} — that lets a
|
|
9
|
+
* builder-authored page declare "when this UI event fires, run these backend
|
|
10
|
+
* actions", plus the {@link EchoEventDispatch} contract a HOST injects to
|
|
11
|
+
* actually carry those actions to a backend (ultimately `runProcess` → gateway).
|
|
12
|
+
*
|
|
13
|
+
* STRUCTURAL COMPATIBILITY (the load-bearing constraint): these types are kept
|
|
14
|
+
* field-for-field compatible with the echothink-fugue registry's `EventBinding`
|
|
15
|
+
* / `ActionNode` contract (see `echothink-component-registry/.../events.tsx` and
|
|
16
|
+
* `echothink-app-renderer/.../logic/graph.ts`). A plain `EthEventBinding[]`
|
|
17
|
+
* array produced here flows through that registry's `eventsAware` HOC →
|
|
18
|
+
* `PageInteractionRuntime.dispatch` → `runActionGraph` → `runProcess` WITHOUT
|
|
19
|
+
* conversion. We deliberately DO NOT import the registry types (that would
|
|
20
|
+
* invert the dependency direction — the registry/renderer depend on the UI
|
|
21
|
+
* library, never the reverse); we mirror the shapes locally instead.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/* -------------------------------------------------------------------------- */
|
|
25
|
+
/* Event names (mirror the registry `EventBinding["event"]` union) */
|
|
26
|
+
/* -------------------------------------------------------------------------- */
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The known UI event names. Kept open (`string & {}`) so custom / semantic
|
|
30
|
+
* events (e.g. `"onRowAction:archive"`) flow through, mirroring the registry's
|
|
31
|
+
* `EventName`.
|
|
32
|
+
*/
|
|
33
|
+
export type EthEventName =
|
|
34
|
+
| "onClick"
|
|
35
|
+
| "onSelect"
|
|
36
|
+
| "onSubmit"
|
|
37
|
+
| "onChange"
|
|
38
|
+
| "onLoad"
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
40
|
+
| (string & {});
|
|
41
|
+
|
|
42
|
+
/* -------------------------------------------------------------------------- */
|
|
43
|
+
/* Action targets / payload */
|
|
44
|
+
/* -------------------------------------------------------------------------- */
|
|
45
|
+
|
|
46
|
+
/** Optimistic-process target reference (the entity the process acts on). */
|
|
47
|
+
export interface EthActionTarget {
|
|
48
|
+
entityType?: string;
|
|
49
|
+
entityId?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The action discriminant — the verbs the host runtime knows how to perform.
|
|
54
|
+
* Kept identical to the registry `ActionNode["action"]` vocabulary (the subset a
|
|
55
|
+
* UI component realistically authors); unknown kinds are tolerated by the
|
|
56
|
+
* runtime, so this stays forward-compatible.
|
|
57
|
+
*/
|
|
58
|
+
export type EthActionKind =
|
|
59
|
+
| "runProcess"
|
|
60
|
+
| "navigate"
|
|
61
|
+
| "setState"
|
|
62
|
+
| "openModal"
|
|
63
|
+
| "closeModal"
|
|
64
|
+
| "showToast";
|
|
65
|
+
|
|
66
|
+
/** Invoke a unit process through the host runtime (ultimately the gateway). */
|
|
67
|
+
export interface EthRunProcessAction {
|
|
68
|
+
kind: "runProcess";
|
|
69
|
+
/** The unit-process name to invoke. */
|
|
70
|
+
process: string;
|
|
71
|
+
target?: EthActionTarget;
|
|
72
|
+
/** Inline payload for the process. May contain `field:` / `route:` refs. */
|
|
73
|
+
payload?: Record<string, unknown>;
|
|
74
|
+
expectedVersion?: number;
|
|
75
|
+
/** Hint that the UI applied an optimistic change before this ran. */
|
|
76
|
+
optimistic?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Client-side navigation to a route, with optional params. */
|
|
80
|
+
export interface EthNavigateAction {
|
|
81
|
+
kind: "navigate";
|
|
82
|
+
/** Target route / href. */
|
|
83
|
+
target: string;
|
|
84
|
+
params?: Record<string, string>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Set a page-scoped state key to a value. */
|
|
88
|
+
export interface EthSetStateAction {
|
|
89
|
+
kind: "setState";
|
|
90
|
+
/** State key to set. */
|
|
91
|
+
target: string;
|
|
92
|
+
value?: unknown;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Open a modal by id. */
|
|
96
|
+
export interface EthOpenModalAction {
|
|
97
|
+
kind: "openModal";
|
|
98
|
+
/** Modal id to open. */
|
|
99
|
+
target: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Close a modal by id. */
|
|
103
|
+
export interface EthCloseModalAction {
|
|
104
|
+
kind: "closeModal";
|
|
105
|
+
/** Modal id to close. */
|
|
106
|
+
target: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Surface a transient toast notification. */
|
|
110
|
+
export interface EthShowToastAction {
|
|
111
|
+
kind: "showToast";
|
|
112
|
+
/** Toast message (the `target` mirrors `message` for field uniformity). */
|
|
113
|
+
message: string;
|
|
114
|
+
level?: "info" | "success" | "error";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The action union an {@link EthEventBinding} carries (discriminant: `kind`). */
|
|
118
|
+
export type EthAction =
|
|
119
|
+
| EthRunProcessAction
|
|
120
|
+
| EthNavigateAction
|
|
121
|
+
| EthSetStateAction
|
|
122
|
+
| EthOpenModalAction
|
|
123
|
+
| EthCloseModalAction
|
|
124
|
+
| EthShowToastAction;
|
|
125
|
+
|
|
126
|
+
/* -------------------------------------------------------------------------- */
|
|
127
|
+
/* The binding */
|
|
128
|
+
/* -------------------------------------------------------------------------- */
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* An event → action(s) binding attached to a component (the value a builder
|
|
132
|
+
* authors). When the named `event` fires the host runs `actions` in order.
|
|
133
|
+
*
|
|
134
|
+
* Structurally compatible with the registry `EventBinding` — see the conversion
|
|
135
|
+
* note in {@link toRegistryEventBinding}: `event` is the same field; the
|
|
136
|
+
* registry calls its action list `actions` with an `action` discriminant, while
|
|
137
|
+
* we use `kind` for ergonomics. {@link toRegistryEventBinding} renames the two
|
|
138
|
+
* fields (`kind`→`action`, `payload`→`externalInput`, `target`→`route`/`modalId`/`key`)
|
|
139
|
+
* so a host that speaks the registry contract can adapt at the edge.
|
|
140
|
+
*/
|
|
141
|
+
export interface EthEventBinding {
|
|
142
|
+
/** The UI event this binding reacts to. */
|
|
143
|
+
event: EthEventName;
|
|
144
|
+
/** The ordered backend/client actions to run when `event` fires. */
|
|
145
|
+
actions: EthAction[];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* -------------------------------------------------------------------------- */
|
|
149
|
+
/* Dispatch contract */
|
|
150
|
+
/* -------------------------------------------------------------------------- */
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Optional dispatch-time data context, structurally compatible with the
|
|
154
|
+
* registry's `DispatchDataContext` / `EventDispatchData` — `row` is the current
|
|
155
|
+
* data-binding row (for `field:` resolution), `route` the route params.
|
|
156
|
+
*/
|
|
157
|
+
export interface EthDispatchData {
|
|
158
|
+
row?: Record<string, unknown>;
|
|
159
|
+
route?: Record<string, string>;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The capability a HOST injects (via {@link EchoEventProvider}) to carry a
|
|
164
|
+
* component's authored binding to the backend. `payload` is the live runtime
|
|
165
|
+
* data captured at the event (e.g. the form values, the selected row, the
|
|
166
|
+
* chosen action id). The host is responsible for merging `payload` into the
|
|
167
|
+
* binding's actions and running them (typically by adapting to the registry
|
|
168
|
+
* action graph → `runProcess` → gateway).
|
|
169
|
+
*
|
|
170
|
+
* Returns the host's promise so callers MAY await completion (e.g. to clear an
|
|
171
|
+
* optimistic flag); a fire-and-forget host returns `void`.
|
|
172
|
+
*/
|
|
173
|
+
export type EchoEventDispatch = (
|
|
174
|
+
binding: EthEventBinding,
|
|
175
|
+
payload?: unknown,
|
|
176
|
+
data?: EthDispatchData,
|
|
177
|
+
) => void | Promise<void>;
|