@gears-frontx/react 0.2.0-alpha.0 → 0.2.0-alpha.15

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/CLAUDE.md CHANGED
@@ -8,47 +8,44 @@ This package is part of the **React Layer (L3)** - it depends only on @gears-fro
8
8
 
9
9
  ## Core Concepts
10
10
 
11
- ### Gears FrontXProvider
11
+ ### FrontXProvider
12
12
 
13
- Wrap your app with Gears FrontXProvider to enable all hooks:
13
+ Wrap your app with FrontXProvider to enable all hooks. A runtime builds exactly one app, once at module level; the provider never creates one. The three compositions below are alternatives: choose one per runtime.
14
14
 
15
15
  ```tsx
16
- import { Gears FrontXProvider } from '@gears-frontx/react';
16
+ import { createFrontX, FrontXProvider, presets } from '@gears-frontx/react';
17
+
18
+ // Alternative 1: the full preset
19
+ const app = createFrontX({ devMode: true }).useAll(presets.full()).build();
17
20
 
18
21
  function App() {
19
22
  return (
20
- <Gears FrontXProvider>
23
+ <FrontXProvider app={app}>
21
24
  <YourApp />
22
- </Gears FrontXProvider>
25
+ </FrontXProvider>
23
26
  );
24
27
  }
25
28
 
26
- // With configuration
27
- <Gears FrontXProvider config={{ devMode: true }}>
28
- <YourApp />
29
- </Gears FrontXProvider>
30
-
31
- // With pre-built app (host-style shell; host typically also uses queryCache() — see REACT.md)
32
- const app = createGears FrontX().use(screensets()).build();
33
- <Gears FrontXProvider app={app}>
34
- <YourApp />
35
- </Gears FrontXProvider>
36
-
37
- // Child MFE app — canonical bootstrap matches src/mfe_packages/*/init.ts and .ai/targets/SCREENSETS.md:
38
- // apiRegistry.register / initialize before .build; createGears FrontX().use(effects()).use(queryCacheShared()).use(mock()).build();
39
- // registerSlice after .build when slices exist.
40
- const mfeApp = createGears FrontX().use(effects()).use(queryCacheShared()).use(mock()).build();
41
- <Gears FrontXProvider app={mfeApp}>
42
- <YourApp />
43
- </Gears FrontXProvider>
29
+ ```
30
+
31
+ Alternative 2: a host shell with a custom plugin set (a host typically uses `queryCache()`):
32
+
33
+ ```tsx
34
+ const app = createFrontX().use(effects()).use(queryCache()).build();
35
+ ```
36
+
37
+ Alternative 3: a child MFE app. The canonical bootstrap matches `src/mfe_packages/*/init.ts`: `apiRegistry.register` / `initialize` before `.build()`, `registerSlice` after `.build()` when slices exist:
38
+
39
+ ```tsx
40
+ const app = createFrontX().use(effects()).use(queryCacheShared()).use(mock()).build();
44
41
  ```
45
42
 
46
43
  The shared `QueryClient` is created and owned by the `queryCache()` framework plugin at L2.
47
- `Gears FrontXProvider` resolves that client from the app instance — it does not create its own `QueryClient`.
44
+ `FrontXProvider` resolves that client from the app instance — it does not create its own `QueryClient`.
48
45
 
49
- When the host uses `queryCache()` and the child MFE app uses `queryCacheShared()` (with `effects()` and `mock()` on the same chain as in repo MFE inits), both roots join the same shared `QueryClient` while keeping separate React trees. `ThemeAwareReactLifecycle` relies on that shared plugin-owned client through the app instance. If the shared client is missing for a mounted MFE, the lifecycle now fails explicitly instead of silently falling back.
46
+ When the host uses `queryCache()` and the child MFE app uses `queryCacheShared()` (with `effects()` and `mock()` on the same chain as in repo MFE inits), both roots join the same shared `QueryClient` while keeping separate React trees. `ThemeAwareReactLifecycle` relies on that shared plugin-owned client through the app instance. If the shared client is missing for a mounted MFE, the lifecycle fails explicitly instead of silently falling back.
50
47
 
51
- For separate roots, build each app with the appropriate query-cache plugin (`queryCache()` for the host, `queryCacheShared()` for child MFE shells) so every tree joins the same cache through plugin composition. MFE package ordering and forbidden bootstrap pieces are defined in SCREENSETS.md. Host apps should register domains/extensions during bootstrap; **`ExtensionDomainSlot`** is the preferred host-side renderer for screen slots while the framework wires `mount_ext`.
48
+ For separate roots, build each app with the appropriate query-cache plugin (`queryCache()` for the host, `queryCacheShared()` for child MFE shells) so every tree joins the same cache through plugin composition. Host apps should register domains/extensions during bootstrap; **`ExtensionDomainSlot`** is the preferred host-side renderer for screen slots while the framework wires `mount_ext`.
52
49
 
53
50
  ### Data Fetching with Endpoint Descriptors
54
51
 
@@ -58,7 +55,7 @@ Services define endpoints as descriptors. Components consume them via `useApiQue
58
55
  import { useApiQuery, useApiMutation, apiRegistry } from '@gears-frontx/react';
59
56
  import { AccountsApiService } from '../api/AccountsApiService';
60
57
 
61
- function ProfileScreen() {
58
+ function ProfileScreen({ teamId }: { teamId?: string }) {
62
59
  const service = apiRegistry.getService(AccountsApiService);
63
60
 
64
61
  // Read — pass descriptor directly
@@ -90,15 +87,23 @@ function ProfileScreen() {
90
87
 
91
88
  // Per-endpoint cache override (rare)
92
89
  const { data: config } = useApiQuery(service.getConfig, { staleTime: 0 });
90
+
91
+ // Conditional query: stays idle until enabled (not on the suspense hooks).
92
+ // The hook still needs a descriptor on every render, so one is built with a
93
+ // stand-in id; while enabled is false it is not fetched automatically, and
94
+ // refetch would still fetch it, so do not call refetch before teamId is set.
95
+ const { data: team } = useApiQuery(service.getTeam({ id: teamId ?? '' }), { enabled: Boolean(teamId) });
93
96
  }
94
97
  ```
95
98
 
96
99
  Cache keys are derived automatically by the service — `QueryCache` methods accept
97
100
  endpoint descriptors directly (e.g., `queryCache.get(service.getCurrentUser)`).
98
101
 
102
+ The `queryCache` a mutation callback receives is bound to the cache scope (mock mode, tenant, session) the mutation started under: once that scope changes, its writes (`set`, `remove`, `invalidate`, `invalidateMany`, `cancel`) do nothing and reads still work, so a response or snapshot from the old scope never lands in the new cache. Nothing else is scope-bound: a `useQueryCache()` handle, the raw `QueryClient`, `cache/*` eventBus emits and per-call `mutate(vars, { onSuccess })` options write to whatever scope is current, so a write after an `await` or inside a mutation callback must go through the callback's `queryCache`.
103
+
99
104
  ### SSE Streaming with Stream Descriptors
100
105
 
101
- Services declare SSE endpoints as stream descriptors. Components consume them via `useApiStream`, which manages the EventSource lifecycle automatically (connect on mount, disconnect on unmount).
106
+ Services declare SSE endpoints as stream descriptors. Components consume them via `useApiStream`, which manages the connection lifecycle automatically (connect on mount, disconnect on unmount).
102
107
 
103
108
  ```tsx
104
109
  import { useApiStream, apiRegistry } from '@gears-frontx/react';
@@ -124,18 +129,38 @@ function ChatStream() {
124
129
  );
125
130
 
126
131
  if (status === 'connecting') return <Loading />;
127
- if (error) return <Error error={error} />;
132
+ if (status === 'error') return <Error error={error} />;
128
133
 
129
134
  return <div>{data?.text}</div>;
130
135
  }
131
136
  ```
132
137
 
133
138
  `useApiStream` returns `{ data, events, status, error, disconnect }`:
134
- - `data` — latest event payload (always set in both modes)
139
+ - `data` — latest event payload (set in both modes)
135
140
  - `events` — all received events when `mode: 'accumulate'`; empty array in `'latest'` mode
136
- - `status` — `'idle' | 'connecting' | 'connected' | 'disconnected' | 'error'`
137
- - `error` — connection error if any
138
- - `disconnect()` — manually close the connection
141
+ - `status` — connection lifecycle state, `StreamStatus` (see below)
142
+ - `error` — the last connect attempt's error; retained until the next connect attempt resets it, so check `status` rather than `error` truthiness
143
+ - `disconnect()` — close the connection manually
144
+
145
+ #### Status lifecycle
146
+
147
+ `status` is a `StreamStatus`: `'idle' | 'connecting' | 'connected' | 'disconnected' | 'error'`.
148
+
149
+ The TSDoc block on `useApiStream` in `src/hooks/useApiStream.ts` is the detailed and authoritative state-machine reference, including teardown edge cases. This guide summarizes consumer-facing usage and gating.
150
+
151
+ `disconnect()` closes the connection and does not reconnect. There is no reconnect function: to open a new connection, change the descriptor key, change `mode`, or toggle `enabled` back to true.
152
+
153
+ #### The `enabled` option
154
+
155
+ `enabled` (default `true`) gates whether the hook connects at all. While it is false the hook never calls the descriptor's `connect()` and ordinarily holds `status: 'idle'` (calling `disconnect()` while disabled is the one exception — see the TSDoc). Flipping it to true re-runs the connect effect and starts a fresh connection; flipping it back to false tears the connection down and returns `status` to `'idle'`.
156
+
157
+ Tearing down via `enabled` does not clear `data` or `events` — a descriptor-key or `mode` change, or re-enabling, does. A component reading `data` while the stream is disabled still sees the last event from the previous connection.
158
+
159
+ Gating advice, since `'idle'` covers both "not started yet" and "deferred by `enabled`":
160
+
161
+ - Gate the loading state on `'connecting'` alone. Treating `'idle'` as loading renders a permanent spinner for a stream deliberately held at `enabled: false`.
162
+ - Handle `'idle'` as "not started / deferred". A component that both defers and shows loading should key its spinner off its own active flag, not off `'idle'`.
163
+ - Check `status === 'error'` rather than the truthiness of `error` to render failures.
139
164
 
140
165
  ### Available Hooks
141
166
 
@@ -150,11 +175,13 @@ function MyComponent() {
150
175
  const app = useFrontX();
151
176
 
152
177
  // Access MFE-enabled registry
153
- const extensions = app.screensetsRegistry.getRegisteredExtensions();
178
+ const extensions = app.mfeRegistry.getExtensionsForDomain('screen');
154
179
 
155
- // Access MFE actions
156
- await app.actions.loadExtension({ extensionId: 'home' });
157
- await app.actions.mountExtension({ extensionId: 'home', domainId: 'screen', container });
180
+ // Access MFE actions — fire-and-forget: each dispatches an actions chain
181
+ // through the acceptance-only registry surface and returns nothing to
182
+ // await.
183
+ app.actions.loadExtension('home');
184
+ app.actions.mountExtension('home');
158
185
  }
159
186
  ```
160
187
 
@@ -240,47 +267,78 @@ Access the MFE bridge for child MFEs:
240
267
 
241
268
  ```tsx
242
269
  import { useMfeBridge } from '@gears-frontx/react';
243
- import { Gears FrontX_ACTION_LOAD_EXT, Gears FrontX_SHARED_PROPERTY_THEME } from '@gears-frontx/react';
270
+ import { FRONTX_ACTION_LOAD_EXT, FRONTX_SHARED_PROPERTY_THEME } from '@gears-frontx/react';
244
271
 
245
272
  function MyExtension() {
246
273
  const bridge = useMfeBridge();
247
274
 
248
- // Execute actions chain on parent
249
- await bridge.executeActionsChain({
250
- action: { type: Gears FrontX_ACTION_LOAD_EXT, target: 'screen', payload: { extensionId: 'other' } }
251
- });
275
+ const handleLoad = () => {
276
+ // executeActionsChain is acceptance-only: it returns void, never
277
+ // throws, and never yields anything to await for the chain's own
278
+ // execution.
279
+ bridge.executeActionsChain({
280
+ action: { type: FRONTX_ACTION_LOAD_EXT, target: 'screen', payload: { subject: 'other' } }
281
+ });
282
+ };
252
283
 
253
284
  // Get shared property
254
- const theme = bridge.getProperty(Gears FrontX_SHARED_PROPERTY_THEME);
285
+ const theme = bridge.getProperty(FRONTX_SHARED_PROPERTY_THEME);
255
286
  }
256
287
  ```
257
288
 
258
289
  #### useSharedProperty
259
290
 
260
- Subscribe to shared property changes:
291
+ Subscribe to shared property changes. Two forms: the context form reads the bridge from `MfeContext`, and the options form accepts an explicit bridge and a fallback.
292
+
293
+ **Context form** — `useSharedProperty(propertyTypeId)`. Reads the bridge from `MfeContext` and throws if no `MfeProvider` is mounted. Returns the value (or `undefined` if the host has not published it), updating on every host publish.
261
294
 
262
295
  ```tsx
263
- import { useSharedProperty, Gears FrontX_SHARED_PROPERTY_THEME } from '@gears-frontx/react';
296
+ import { useSharedProperty, FRONTX_SHARED_PROPERTY_THEME } from '@gears-frontx/react';
264
297
 
265
298
  function ThemedComponent() {
266
- const theme = useSharedProperty(Gears FrontX_SHARED_PROPERTY_THEME);
299
+ const theme = useSharedProperty(FRONTX_SHARED_PROPERTY_THEME);
267
300
 
268
301
  return <div style={{ backgroundColor: theme?.primaryColor }}>...</div>;
269
302
  }
270
303
  ```
271
304
 
305
+ **Options form** — `useSharedProperty(propertyTypeId, { bridge, fallback })`. Use it for components that never mount `MfeProvider`, or to show a value until the host publishes one.
306
+
307
+ ```tsx
308
+ import { useSharedProperty } from '@gears-frontx/react';
309
+ import type { ChildMfeBridge } from '@gears-frontx/framework';
310
+
311
+ function StandaloneComponent({ bridge }: { bridge: ChildMfeBridge }) {
312
+ const label = useSharedProperty<string>(LABEL_PROPERTY_ID, {
313
+ bridge,
314
+ fallback: 'Untitled',
315
+ });
316
+
317
+ return <span>{label}</span>;
318
+ }
319
+ ```
320
+
321
+ `UseSharedPropertyOptions<T>` fields:
322
+
323
+ - `bridge` — bridge to read the property from, bypassing `MfeContext`. Pass it when the calling component never mounts `MfeProvider` (for example, a component that receives its bridge as a prop or from a non-React entry point). When omitted, the hook falls back to the bridge from `MfeContext` and throws if no `MfeProvider` is mounted. Changing `bridge` or `propertyTypeId` moves the subscription to the new source and the returned value follows on the same render.
324
+ - `fallback` — value returned while the host has not published the property yet. Only `undefined` is substituted; a published `null` is a real value and comes through as-is. With `fallback` set the hook returns `T` rather than `T | undefined`. It may be passed inline on every render without affecting the subscription.
325
+
326
+ There is no consumer-side validation option on purpose: the host validates every published value against the property's schema before it reaches any MFE (`TypeSystemPlugin.register()` throws on a mismatch and the update is never stored or delivered). `T` is the caller's declaration of that schema, as with `useState<T>`.
327
+
328
+ Both forms read through `useSyncExternalStore`, so the first render shows the host's current value and the result is tearing-protected under concurrent rendering. The TSDoc block on `useSharedProperty` in `src/mfe/hooks/useSharedProperty.ts` is the authoritative reference; this guide summarizes consumer-facing usage.
329
+
272
330
  #### useHostAction
273
331
 
274
332
  Invoke actions on the host application:
275
333
 
276
334
  ```tsx
277
- import { useHostAction, Gears FrontX_ACTION_LOAD_EXT } from '@gears-frontx/react';
335
+ import { useHostAction, FRONTX_ACTION_LOAD_EXT } from '@gears-frontx/react';
278
336
 
279
337
  function MyExtension() {
280
- const loadExtension = useHostAction(Gears FrontX_ACTION_LOAD_EXT);
338
+ const loadExtension = useHostAction(FRONTX_ACTION_LOAD_EXT);
281
339
 
282
340
  const handleClick = () => {
283
- loadExtension({ extensionId: 'other' });
341
+ loadExtension({ subject: 'other' });
284
342
  };
285
343
 
286
344
  return <button onClick={handleClick}>Load Extension</button>;
@@ -414,7 +472,7 @@ function Layout() {
414
472
 
415
473
  ## Key Rules
416
474
 
417
- 1. **Wrap with Gears FrontXProvider** - Required for all hooks to work
475
+ 1. **Wrap with FrontXProvider** - Required for all hooks to work
418
476
  2. **Use hooks for state access** - Don't import selectors directly from @gears-frontx/framework
419
477
  3. **Use endpoint descriptors for data** - `useApiQuery(service.endpoint)` for REST, `useApiStream(service.stream)` for SSE — not `queryOptions()` or manual key factories
420
478
  4. **Service is the cache contract** - The service IS the data layer; cache keys are derived automatically
@@ -438,7 +496,7 @@ This allows users to import everything from `@gears-frontx/react` without needin
438
496
  ## Exports
439
497
 
440
498
  ### Components
441
- - `Gears FrontXProvider` - Main context provider
499
+ - `FrontXProvider` - Main context provider
442
500
  - `MfeProvider` - MFE context provider
443
501
  - `ExtensionDomainSlot` - Domain slot renderer
444
502
  - `RefContainerProvider` - Container reference provider
@@ -455,18 +513,18 @@ This allows users to import everything from `@gears-frontx/react` without needin
455
513
  - `useScreenTranslations` - Screen translation loading
456
514
  - `useTheme` - Theme utilities
457
515
  - `useMfeBridge` - Access MFE bridge
458
- - `useSharedProperty` - Subscribe to shared property
516
+ - `useSharedProperty` - Subscribe to shared property, via `MfeContext` or an explicit bridge
459
517
  - `useHostAction` - Invoke host action
460
518
  - `useDomainExtensions` - Subscribe to domain extensions
461
519
  - `useRegisteredPackages` - Subscribe to registered GTS packages
462
520
  - `useActivePackage` - Subscribe to active GTS package
463
521
 
464
522
  ### Context
465
- - `Gears FrontXContext` - React context (for advanced use)
523
+ - `FrontXContext` - React context (for advanced use)
466
524
  - `MfeContext` - MFE context (for advanced use)
467
525
 
468
526
  ### Types
469
- - `Gears FrontXProviderProps`
527
+ - `FrontXProviderProps`
470
528
  - `ApiQueryResult<TData>` - Gears FrontX-owned query result type (data, error, isLoading, refetch, etc.)
471
529
  - `ApiMutationResult<TData>` - Gears FrontX-owned mutation result type (mutateAsync, isPending, error, reset, etc.)
472
530
  - `ApiStreamResult<TEvent>` - Gears FrontX-owned stream result type (data, events, status, error, disconnect)
@@ -474,6 +532,7 @@ This allows users to import everything from `@gears-frontx/react` without needin
474
532
  - `QueryCache` - Restricted cache interface (accepts descriptors or raw keys)
475
533
  - `MutationCallbackContext` - Context with queryCache injected into mutation callbacks
476
534
  - `MfeProviderProps`, `ExtensionDomainSlotProps`
535
+ - `UseSharedPropertyOptions<T>` - Options for `useSharedProperty` (bridge, fallback)
477
536
  - `UseTranslationReturn`, `UseThemeReturn`
478
537
  - All types from @gears-frontx/framework (including `EndpointDescriptor`, `MutationDescriptor`, `StreamDescriptor`, `StreamStatus`)
479
538
 
@@ -509,13 +568,11 @@ function MyComponent() {
509
568
  const app = useFrontX();
510
569
  const containerRef = useRef<HTMLDivElement>(null);
511
570
 
512
- const handleNavigate = async () => {
571
+ const handleNavigate = () => {
513
572
  if (containerRef.current) {
514
- await app.actions.mountExtension({
515
- extensionId: 'home',
516
- domainId: 'screen',
517
- container: containerRef.current,
518
- });
573
+ // Fire-and-forget: dispatches an actions chain through the
574
+ // acceptance-only registry surface and returns nothing to await.
575
+ app.actions.mountExtension('home');
519
576
  }
520
577
  };
521
578
 
@@ -535,7 +592,7 @@ import { ExtensionDomainSlot } from '@gears-frontx/react';
535
592
  function MyComponent() {
536
593
  return (
537
594
  <ExtensionDomainSlot
538
- registry={app.screensetsRegistry}
595
+ registry={app.mfeRegistry}
539
596
  domainId="screen"
540
597
  extensionId="home"
541
598
  loadingComponent={<Loading />}