e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
138 lines
5.9 KiB
Markdown
138 lines
5.9 KiB
Markdown
# @assistant-ui/store Specification
|
|
|
|
React integration for tap. Type-safe client-based state via module augmentation.
|
|
|
|
## Types
|
|
|
|
### ScopeRegistry
|
|
```typescript
|
|
declare module "@assistant-ui/store" {
|
|
interface ScopeRegistry {
|
|
name: {
|
|
methods: MethodsType; // must include getState(): StateType
|
|
meta?: { source: ClientNames; query: QueryType };
|
|
events?: { "name.event": PayloadType };
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
### Core Types
|
|
```typescript
|
|
type ClientOutput<K> = ClientSchemas[K]["methods"] & ClientMethods;
|
|
type ClientMethods = { [key: string]: (...args: any[]) => any };
|
|
type AssistantClientAccessor<K> = (() => Methods<K>) & ({ source; query } | { source: "root"; query: {} } | { source: null; query: null });
|
|
type AssistantClient = { [K]: AssistantClientAccessor<K>; subscribe(cb): Unsubscribe; on(selector, cb): Unsubscribe };
|
|
type AssistantState = { [K]: ReturnType<ClientSchemas[K]["methods"]["getState"]> };
|
|
```
|
|
|
|
## API
|
|
|
|
### useAui
|
|
```typescript
|
|
useAui(): AssistantClient;
|
|
useAui(clients: { [K]?: ClientElement<K> | DerivedElement<K> }): AssistantClient;
|
|
```
|
|
Flow: splitClients → apply transformScopes → mount root clients → create derived accessors → merge with parent.
|
|
|
|
### useAuiState
|
|
```typescript
|
|
useAuiState<T>(selector: (state: AssistantState) => T): T;
|
|
```
|
|
`useSyncExternalStore` with proxied state. **Throws** if selector returns proxy (must return specific value).
|
|
|
|
### useAuiEvent
|
|
```typescript
|
|
useAuiEvent<E>(selector: E | { scope: EventScope<E>; event: E }, callback: (payload) => void): void;
|
|
```
|
|
Selectors: `"client.event"` | `{ scope: "parent", event }` | `{ scope: "*", event }`. Wildcard `"*"` receives all.
|
|
|
|
### AuiProvider / AuiIf
|
|
```typescript
|
|
<AuiProvider value={aui}>{children}</AuiProvider>
|
|
<AuiIf condition={(s) => boolean}>{children}</AuiIf>
|
|
```
|
|
|
|
### Derived
|
|
```typescript
|
|
Derived<K>({ source, query, get: (client) => methods });
|
|
```
|
|
Returns marker element. Meta (`source`, `query`) keys the derived scope's resource fiber — a different meta yields a new client function in the same render pass, so consumers see the new derivation immediately rather than after the next commit.
|
|
|
|
### attachTransformScopes
|
|
```typescript
|
|
attachTransformScopes(resource, (scopes, parent) => newScopes): void;
|
|
```
|
|
Attaches a function that receives the current scopes config and the parent `AssistantClient`, and returns a new scopes config. The transform can inspect `parent[key].source` to check whether a scope exists in parent context (`null` = not provided). Transforms are collected from root elements and run iteratively (new root elements added by transforms are also processed). Single transform per resource; throws on duplicate attach.
|
|
|
|
### useAssistantClientRef / useAssistantEmit
|
|
```typescript
|
|
useAssistantClientRef(): { current: AssistantClient };
|
|
useAssistantEmit(): <E>(event: E, payload) => void; // Stable via useEffectEvent
|
|
```
|
|
|
|
### useClientResource
|
|
```typescript
|
|
useClientResource(element: ResourceElement<TMethods>): { state: InferClientState<TMethods>; methods: TMethods; key: string | number | undefined };
|
|
```
|
|
Wraps resource element to create stable client proxy. Adds client to stack for event scoping. Use for 1:1 client mappings. State is inferred from the `getState()` method if present.
|
|
|
|
### useClientLookup
|
|
```typescript
|
|
useClientLookup<TMethods extends ClientMethods>(
|
|
getElements: () => readonly ResourceElement<TMethods>[],
|
|
getElementsDeps: readonly unknown[]
|
|
): { state: InferClientState<TMethods>[]; get: (lookup: { index: number } | { key: string }) => TMethods };
|
|
```
|
|
Wraps each element with `useClientResource`. Throws on lookup miss.
|
|
|
|
### useClientList
|
|
```typescript
|
|
useClientList<TData, TMethods extends ClientMethods>({
|
|
initialValues: TData[];
|
|
getKey: (data: TData) => string;
|
|
resource: ContravariantResource<TMethods, ResourceProps<TData>>;
|
|
}): { state: InferClientState<TMethods>[]; get: (lookup: { index: number } | { key: string }) => TMethods; add: (data: TData) => void };
|
|
|
|
type ResourceProps<TData> = { key: string; getInitialData: () => TData; remove: () => void };
|
|
```
|
|
Wraps useClientLookup. `getInitialData` may only be called once. Throws on duplicate key add.
|
|
|
|
## Events
|
|
|
|
```typescript
|
|
type AssistantEventName = keyof ClientEventMap | "*";
|
|
type AssistantEventScope<E> = "*" | EventSource<E> | AncestorsOf<EventSource<E>>;
|
|
type AssistantEventSelector<E> = E | { scope: Scope<E>; event: E };
|
|
```
|
|
Flow: `useAssistantEmit` captures client stack → `emit` queues via microtask → NotificationManager notifies → scope filtering.
|
|
|
|
## Implementation
|
|
|
|
| Component | Behavior |
|
|
|-----------|----------|
|
|
| **useClientResource** | Mounts element → stable proxy via `useMemo` → delegates to ref → `SYMBOL_GET_OUTPUT` for internal access |
|
|
| **ProxiedState** | Proxy intercepts `state.foo` → `aui.foo()` → `SYMBOL_GET_OUTPUT` |
|
|
| **Client Stack** | Context stack per level. Emit captures stack. Listeners filter by matching stack |
|
|
| **NotificationManager** | Handles events (`on`/`emit`) and state subscriptions (`subscribe`/`notifySubscribers`) |
|
|
| **splitClients** | Separate root/derived → collect and run `transformScopes` iteratively → filter parent-provided scopes |
|
|
|
|
## Design
|
|
|
|
| Audience | API Surface |
|
|
|----------|-------------|
|
|
| Users | `useAui`, `useAuiState`, `useAuiEvent`, `AuiProvider`, `AuiIf`, `Derived` |
|
|
| Authors | Above + `use*`, `attachTransformScopes`, `ClientOutput`, `ScopeRegistry` |
|
|
| Internal | `utils/*` |
|
|
|
|
**Terminology**: Client (React Query pattern), methods (not actions), meta (optional source/query), events (optional).
|
|
|
|
## Invariants
|
|
|
|
1. `ScopeRegistry` must have ≥1 client (compile error otherwise)
|
|
2. Resources return methods object matching `ClientOutput<K>` (with `getState()` for state access)
|
|
3. Events: `"clientName.eventName"` format
|
|
4. `meta.source` must be valid `ClientNames`
|
|
5. `useAuiState` selector cannot return whole state
|
|
6. Single transformScopes per resource; transform receives `(scopes, parent)` to inspect parent context
|