@assistant-ui/store 0.0.1 → 0.0.3

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.
Files changed (155) hide show
  1. package/README.md +59 -262
  2. package/dist/AssistantIf.d.ts +10 -0
  3. package/dist/AssistantIf.d.ts.map +1 -0
  4. package/dist/AssistantIf.js +13 -0
  5. package/dist/AssistantIf.js.map +1 -0
  6. package/dist/Derived.d.ts +34 -0
  7. package/dist/Derived.d.ts.map +1 -0
  8. package/dist/Derived.js +11 -0
  9. package/dist/Derived.js.map +1 -0
  10. package/dist/attachDefaultPeers.d.ts +56 -0
  11. package/dist/attachDefaultPeers.d.ts.map +1 -0
  12. package/dist/attachDefaultPeers.js +22 -0
  13. package/dist/attachDefaultPeers.js.map +1 -0
  14. package/dist/index.d.ts +11 -14
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +19 -16
  17. package/dist/index.js.map +1 -1
  18. package/dist/tapClientList.d.ts +24 -0
  19. package/dist/tapClientList.d.ts.map +1 -0
  20. package/dist/tapClientList.js +72 -0
  21. package/dist/tapClientList.js.map +1 -0
  22. package/dist/tapClientLookup.d.ts +11 -0
  23. package/dist/tapClientLookup.d.ts.map +1 -0
  24. package/dist/tapClientLookup.js +42 -0
  25. package/dist/tapClientLookup.js.map +1 -0
  26. package/dist/tapClientResource.d.ts +24 -0
  27. package/dist/tapClientResource.d.ts.map +1 -0
  28. package/dist/tapClientResource.js +100 -0
  29. package/dist/tapClientResource.js.map +1 -0
  30. package/dist/types/client.d.ts +117 -0
  31. package/dist/types/client.d.ts.map +1 -0
  32. package/dist/types/client.js +1 -0
  33. package/dist/types/events.d.ts +33 -0
  34. package/dist/types/events.d.ts.map +1 -0
  35. package/dist/types/events.js +12 -0
  36. package/dist/types/events.js.map +1 -0
  37. package/dist/useAssistantClient.d.ts +13 -41
  38. package/dist/useAssistantClient.d.ts.map +1 -1
  39. package/dist/useAssistantClient.js +185 -130
  40. package/dist/useAssistantClient.js.map +1 -1
  41. package/dist/useAssistantEvent.d.ts +2 -2
  42. package/dist/useAssistantEvent.d.ts.map +1 -1
  43. package/dist/useAssistantEvent.js +3 -6
  44. package/dist/useAssistantEvent.js.map +1 -1
  45. package/dist/useAssistantState.d.ts +2 -2
  46. package/dist/useAssistantState.d.ts.map +1 -1
  47. package/dist/useAssistantState.js +7 -36
  48. package/dist/useAssistantState.js.map +1 -1
  49. package/dist/utils/BaseProxyHandler.d.ts +23 -0
  50. package/dist/utils/BaseProxyHandler.d.ts.map +1 -0
  51. package/dist/utils/BaseProxyHandler.js +41 -0
  52. package/dist/utils/BaseProxyHandler.js.map +1 -0
  53. package/dist/utils/NotificationManager.d.ts +11 -0
  54. package/dist/utils/NotificationManager.d.ts.map +1 -0
  55. package/dist/utils/NotificationManager.js +81 -0
  56. package/dist/utils/NotificationManager.js.map +1 -0
  57. package/dist/utils/StoreResource.d.ts +14 -0
  58. package/dist/utils/StoreResource.d.ts.map +1 -0
  59. package/dist/utils/StoreResource.js +23 -0
  60. package/dist/utils/StoreResource.js.map +1 -0
  61. package/dist/utils/proxied-assistant-state.d.ts +8 -0
  62. package/dist/utils/proxied-assistant-state.d.ts.map +1 -0
  63. package/dist/utils/proxied-assistant-state.js +41 -0
  64. package/dist/utils/proxied-assistant-state.js.map +1 -0
  65. package/dist/{AssistantContext.d.ts → utils/react-assistant-context.d.ts} +6 -6
  66. package/dist/utils/react-assistant-context.d.ts.map +1 -0
  67. package/dist/utils/react-assistant-context.js +73 -0
  68. package/dist/utils/react-assistant-context.js.map +1 -0
  69. package/dist/utils/splitClients.d.ts +28 -0
  70. package/dist/utils/splitClients.d.ts.map +1 -0
  71. package/dist/utils/splitClients.js +37 -0
  72. package/dist/utils/splitClients.js.map +1 -0
  73. package/dist/utils/tap-assistant-context.d.ts +19 -0
  74. package/dist/utils/tap-assistant-context.d.ts.map +1 -0
  75. package/dist/utils/tap-assistant-context.js +37 -0
  76. package/dist/utils/tap-assistant-context.js.map +1 -0
  77. package/dist/utils/tap-client-stack-context.d.ts +23 -0
  78. package/dist/utils/tap-client-stack-context.d.ts.map +1 -0
  79. package/dist/utils/tap-client-stack-context.js +30 -0
  80. package/dist/utils/tap-client-stack-context.js.map +1 -0
  81. package/package.json +5 -6
  82. package/src/AssistantIf.tsx +17 -0
  83. package/src/Derived.ts +46 -0
  84. package/src/attachDefaultPeers.ts +78 -0
  85. package/src/index.ts +31 -25
  86. package/src/tapClientList.ts +105 -0
  87. package/src/tapClientLookup.ts +56 -0
  88. package/src/tapClientResource.ts +152 -0
  89. package/src/types/client.ts +186 -0
  90. package/src/types/events.ts +77 -0
  91. package/src/useAssistantClient.tsx +259 -217
  92. package/src/useAssistantEvent.ts +6 -9
  93. package/src/useAssistantState.tsx +10 -48
  94. package/src/utils/BaseProxyHandler.ts +50 -0
  95. package/src/utils/NotificationManager.ts +110 -0
  96. package/src/utils/StoreResource.ts +36 -0
  97. package/src/utils/proxied-assistant-state.tsx +53 -0
  98. package/src/utils/react-assistant-context.tsx +107 -0
  99. package/src/utils/splitClients.ts +85 -0
  100. package/src/utils/tap-assistant-context.ts +59 -0
  101. package/src/utils/tap-client-stack-context.ts +51 -0
  102. package/dist/AssistantContext.d.ts.map +0 -1
  103. package/dist/AssistantContext.js +0 -45
  104. package/dist/AssistantContext.js.map +0 -1
  105. package/dist/DerivedScope.d.ts +0 -18
  106. package/dist/DerivedScope.d.ts.map +0 -1
  107. package/dist/DerivedScope.js +0 -11
  108. package/dist/DerivedScope.js.map +0 -1
  109. package/dist/EventContext.d.ts +0 -65
  110. package/dist/EventContext.d.ts.map +0 -1
  111. package/dist/EventContext.js +0 -62
  112. package/dist/EventContext.js.map +0 -1
  113. package/dist/ScopeRegistry.d.ts +0 -41
  114. package/dist/ScopeRegistry.d.ts.map +0 -1
  115. package/dist/ScopeRegistry.js +0 -17
  116. package/dist/ScopeRegistry.js.map +0 -1
  117. package/dist/StoreContext.d.ts +0 -9
  118. package/dist/StoreContext.d.ts.map +0 -1
  119. package/dist/StoreContext.js +0 -20
  120. package/dist/StoreContext.js.map +0 -1
  121. package/dist/asStore.d.ts +0 -20
  122. package/dist/asStore.d.ts.map +0 -1
  123. package/dist/asStore.js +0 -23
  124. package/dist/asStore.js.map +0 -1
  125. package/dist/tapApi.d.ts +0 -36
  126. package/dist/tapApi.d.ts.map +0 -1
  127. package/dist/tapApi.js +0 -52
  128. package/dist/tapApi.js.map +0 -1
  129. package/dist/tapLookupResources.d.ts +0 -44
  130. package/dist/tapLookupResources.d.ts.map +0 -1
  131. package/dist/tapLookupResources.js +0 -21
  132. package/dist/tapLookupResources.js.map +0 -1
  133. package/dist/tapStoreList.d.ts +0 -76
  134. package/dist/tapStoreList.d.ts.map +0 -1
  135. package/dist/tapStoreList.js +0 -46
  136. package/dist/tapStoreList.js.map +0 -1
  137. package/dist/types.d.ts +0 -88
  138. package/dist/types.d.ts.map +0 -1
  139. package/dist/types.js +0 -1
  140. package/dist/utils/splitScopes.d.ts +0 -24
  141. package/dist/utils/splitScopes.d.ts.map +0 -1
  142. package/dist/utils/splitScopes.js +0 -18
  143. package/dist/utils/splitScopes.js.map +0 -1
  144. package/src/AssistantContext.tsx +0 -64
  145. package/src/DerivedScope.ts +0 -21
  146. package/src/EventContext.ts +0 -184
  147. package/src/ScopeRegistry.ts +0 -58
  148. package/src/StoreContext.ts +0 -28
  149. package/src/asStore.ts +0 -40
  150. package/src/tapApi.ts +0 -91
  151. package/src/tapLookupResources.ts +0 -62
  152. package/src/tapStoreList.ts +0 -133
  153. package/src/types.ts +0 -129
  154. package/src/utils/splitScopes.ts +0 -38
  155. /package/dist/{types.js.map → types/client.js.map} +0 -0
@@ -0,0 +1,78 @@
1
+ import type { ResourceElement } from "@assistant-ui/tap";
2
+ import type { ClientElement, ClientNames } from "./types/client";
3
+ import type { DerivedElement } from "./Derived";
4
+
5
+ /**
6
+ * Symbol used to store default peer clients on a resource.
7
+ */
8
+ const DEFAULT_PEERS = Symbol("assistant-ui.default-peers");
9
+
10
+ /**
11
+ * Type for resources that have default peers attached.
12
+ */
13
+ export type ResourceWithDefaultPeers = {
14
+ [DEFAULT_PEERS]?: DefaultPeers;
15
+ };
16
+
17
+ /**
18
+ * Default peers configuration - can be either root clients or derived clients.
19
+ */
20
+ export type DefaultPeers = {
21
+ [K in ClientNames]?: ClientElement<K> | DerivedElement<K>;
22
+ };
23
+
24
+ /**
25
+ * Attaches default peer clients to a resource.
26
+ *
27
+ * Default peers are only applied if the scope doesn't exist:
28
+ * - Not defined in parent context
29
+ * - Not provided by user
30
+ * - Not already defined by a previous resource's default peers
31
+ *
32
+ * First definition wins - no overriding is permitted.
33
+ *
34
+ * @param resource - The resource to attach default peers to
35
+ * @param peers - The default peer clients to attach
36
+ * @throws Error if a peer key already exists in the resource's default peers
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * const ThreadListClient = resource(({ ... }) => { ... });
41
+ *
42
+ * attachDefaultPeers(ThreadListClient, {
43
+ * // Derived default peers
44
+ * thread: Derived({ source: "threads", query: { type: "main" }, get: ... }),
45
+ * threadListItem: Derived({ ... }),
46
+ * composer: Derived({ getMeta: ..., get: ... }),
47
+ *
48
+ * // Root default peers
49
+ * tools: Tools({}),
50
+ * modelContext: ModelContext({}),
51
+ * });
52
+ * ```
53
+ */
54
+ export function attachDefaultPeers<
55
+ T extends (...args: any[]) => ResourceElement<any>,
56
+ >(resource: T, peers: DefaultPeers): void {
57
+ const resourceWithPeers = resource as T & ResourceWithDefaultPeers;
58
+ const existing = resourceWithPeers[DEFAULT_PEERS] ?? {};
59
+
60
+ for (const key of Object.keys(peers)) {
61
+ if (key in existing) {
62
+ throw new Error(
63
+ `Default peer "${key}" is already attached to this resource`,
64
+ );
65
+ }
66
+ }
67
+
68
+ resourceWithPeers[DEFAULT_PEERS] = { ...existing, ...peers };
69
+ }
70
+
71
+ /**
72
+ * Gets the default peers attached to a resource, if any.
73
+ */
74
+ export function getDefaultPeers<
75
+ T extends (...args: any[]) => ResourceElement<any>,
76
+ >(resource: T): DefaultPeers | undefined {
77
+ return (resource as T & ResourceWithDefaultPeers)[DEFAULT_PEERS];
78
+ }
package/src/index.ts CHANGED
@@ -1,31 +1,37 @@
1
+ // hooks
1
2
  export { useAssistantClient } from "./useAssistantClient";
2
3
  export { useAssistantState } from "./useAssistantState";
3
- export { AssistantProvider } from "./AssistantContext";
4
- export type { AssistantScopes, AssistantClient, AssistantState } from "./types";
5
- export { DerivedScope } from "./DerivedScope";
6
- export type { ApiObject } from "./tapApi";
7
- export { tapApi } from "./tapApi";
8
- export { tapLookupResources } from "./tapLookupResources";
9
- export { tapStoreList } from "./tapStoreList";
10
- export type { TapStoreListConfig } from "./tapStoreList";
11
- export { registerAssistantScope } from "./ScopeRegistry";
4
+ export { useAssistantEvent } from "./useAssistantEvent";
12
5
 
13
- export type { AssistantScopeRegistry } from "./types";
6
+ // components
7
+ export { AssistantIf } from "./AssistantIf";
8
+ export { AssistantProvider } from "./utils/react-assistant-context";
14
9
 
15
- // Events & Store Context
16
- export { tapStoreContext } from "./StoreContext";
17
- export type { StoreContextValue } from "./StoreContext";
18
- export { useAssistantEvent } from "./useAssistantEvent";
19
- export { normalizeEventSelector, checkEventScope } from "./EventContext";
10
+ // resources
11
+ export { Derived } from "./Derived";
12
+ export { attachDefaultPeers } from "./attachDefaultPeers";
13
+
14
+ // tap hooks
15
+ export {
16
+ tapAssistantClientRef,
17
+ tapAssistantEmit,
18
+ } from "./utils/tap-assistant-context";
19
+ export { tapClientResource } from "./tapClientResource";
20
+ export { tapClientLookup } from "./tapClientLookup";
21
+ export { tapClientList } from "./tapClientList";
22
+
23
+ // types
20
24
  export type {
21
- AssistantEvent,
22
- AssistantEventRegistry,
23
- AssistantEventScopeConfig,
24
- AssistantEventMap,
25
- AssistantEventScope,
26
- AssistantEventSelector,
25
+ ClientRegistry,
26
+ ClientOutput,
27
+ AssistantClient,
28
+ AssistantState,
29
+ } from "./types/client";
30
+ export type {
31
+ AssistantEventName,
27
32
  AssistantEventCallback,
28
- EventSource,
29
- SourceByScope,
30
- EventManager,
31
- } from "./EventContext";
33
+ AssistantEventPayload,
34
+ AssistantEventSelector,
35
+ AssistantEventScope,
36
+ } from "./types/events";
37
+ export type { DefaultPeers } from "./attachDefaultPeers";
@@ -0,0 +1,105 @@
1
+ import { tapState } from "@assistant-ui/tap";
2
+ import type { ContravariantResource } from "@assistant-ui/tap";
3
+ import { tapClientLookup } from "./tapClientLookup";
4
+ import type { ClientMethods, ClientOutputOf } from "./types/client";
5
+
6
+ const createProps = <TData>(
7
+ key: string,
8
+ data: TData,
9
+ remove: () => void,
10
+ ): tapClientList.ResourceProps<TData> => {
11
+ let initialData: { data: TData } | undefined = { data };
12
+ return {
13
+ key,
14
+ getInitialData: () => {
15
+ if (!initialData) {
16
+ throw new Error("getInitialData may only be called once");
17
+ }
18
+ const data = initialData.data;
19
+ initialData = undefined;
20
+ return data;
21
+ },
22
+ remove,
23
+ };
24
+ };
25
+
26
+ export const tapClientList = <TData, TState, TMethods extends ClientMethods>(
27
+ props: tapClientList.Props<TData, TState, TMethods>,
28
+ ): {
29
+ state: TState[];
30
+ get: (lookup: { index: number } | { key: string }) => TMethods;
31
+ add: (initialData: TData) => void;
32
+ } => {
33
+ const { initialValues, getKey, resource: Resource } = props;
34
+
35
+ type Props = tapClientList.ResourceProps<TData>;
36
+
37
+ const [items, setItems] = tapState<Record<string, Props>>(() => {
38
+ const entries: [string, Props][] = [];
39
+ for (const data of initialValues) {
40
+ const key = getKey(data);
41
+ entries.push([
42
+ key,
43
+ createProps(key, data, () => {
44
+ setItems((items) => {
45
+ const newItems = { ...items };
46
+ delete newItems[key];
47
+ return newItems;
48
+ });
49
+ }),
50
+ ]);
51
+ }
52
+ return Object.fromEntries(entries);
53
+ });
54
+
55
+ const lookup = tapClientLookup<TState, TMethods, Record<string, Props>>(
56
+ items,
57
+ Resource,
58
+ [Resource],
59
+ );
60
+
61
+ const add = (data: TData) => {
62
+ const key = getKey(data);
63
+ setItems((items) => {
64
+ if (key in items) {
65
+ throw new Error(
66
+ `Tried to add item with a key ${key} that already exists`,
67
+ );
68
+ }
69
+
70
+ return {
71
+ ...items,
72
+ [key]: createProps(key, data, () => {
73
+ setItems((items) => {
74
+ const newItems = { ...items };
75
+ delete newItems[key];
76
+ return newItems;
77
+ });
78
+ }),
79
+ };
80
+ });
81
+ };
82
+
83
+ return {
84
+ state: lookup.state,
85
+ get: lookup.get,
86
+ add,
87
+ };
88
+ };
89
+
90
+ export namespace tapClientList {
91
+ export type ResourceProps<TData> = {
92
+ key: string;
93
+ getInitialData: () => TData;
94
+ remove: () => void;
95
+ };
96
+
97
+ export type Props<TData, TState, TMethods extends ClientMethods> = {
98
+ initialValues: TData[];
99
+ getKey: (data: TData) => string;
100
+ resource: ContravariantResource<
101
+ ClientOutputOf<TState, TMethods>,
102
+ ResourceProps<TData>
103
+ >;
104
+ };
105
+ }
@@ -0,0 +1,56 @@
1
+ import { ResourceElement, tapMemo, tapResources } from "@assistant-ui/tap";
2
+ import type { ClientMethods, ClientOutputOf } from "./types/client";
3
+ import { ClientResource } from "./tapClientResource";
4
+
5
+ export const tapClientLookup = <
6
+ TState,
7
+ TMethods extends ClientMethods,
8
+ M extends Record<string | number | symbol, any>,
9
+ >(
10
+ map: M,
11
+ getElement: (
12
+ t: M[keyof M],
13
+ key: keyof M,
14
+ ) => ResourceElement<ClientOutputOf<TState, TMethods>>,
15
+ getElementDeps: any[],
16
+ ): {
17
+ state: TState[];
18
+ get: (lookup: { index: number } | { key: keyof M }) => TMethods;
19
+ } => {
20
+ const resources = tapResources(
21
+ map,
22
+ (t, key) => ClientResource(getElement(t, key)),
23
+ getElementDeps,
24
+ );
25
+ const keys = tapMemo(() => Object.keys(map) as (keyof M)[], [map]);
26
+
27
+ const state = tapMemo(() => {
28
+ const result = new Array(keys.length);
29
+ for (let i = 0; i < keys.length; i++) {
30
+ result[i] = resources[keys[i]!].state;
31
+ }
32
+ return result;
33
+ }, [keys, resources]);
34
+
35
+ return {
36
+ state,
37
+ get: (lookup: { index: number } | { key: keyof M }) => {
38
+ if ("index" in lookup) {
39
+ if (lookup.index < 0 || lookup.index >= keys.length) {
40
+ throw new Error(
41
+ `tapClientLookup: Index ${lookup.index} out of bounds (length: ${keys.length})`,
42
+ );
43
+ }
44
+ return resources[keys[lookup.index]!]!.methods;
45
+ }
46
+
47
+ const value = resources[lookup.key];
48
+ if (!value) {
49
+ throw new Error(
50
+ `tapClientLookup: Key "${String(lookup.key)}" not found`,
51
+ );
52
+ }
53
+ return value.methods;
54
+ },
55
+ };
56
+ };
@@ -0,0 +1,152 @@
1
+ import {
2
+ tapEffect,
3
+ tapMemo,
4
+ tapRef,
5
+ type ResourceElement,
6
+ tapResource,
7
+ resource,
8
+ tapInlineResource,
9
+ } from "@assistant-ui/tap";
10
+ import type { ClientMethods, ClientOutputOf } from "./types/client";
11
+ import {
12
+ tapClientStack,
13
+ tapWithClientStack,
14
+ SYMBOL_CLIENT_INDEX,
15
+ } from "./utils/tap-client-stack-context";
16
+ import {
17
+ BaseProxyHandler,
18
+ handleIntrospectionProp,
19
+ } from "./utils/BaseProxyHandler";
20
+
21
+ /**
22
+ * Symbol used internally to get state from ClientProxy.
23
+ * This allows getState() to be optional in the user-facing client.
24
+ */
25
+ const SYMBOL_GET_OUTPUT = Symbol("assistant-ui.store.getValue");
26
+
27
+ type ClientInternal = {
28
+ [SYMBOL_GET_OUTPUT]: ClientOutputOf<unknown, ClientMethods>;
29
+ };
30
+
31
+ export const getClientState = (client: ClientMethods) => {
32
+ const output = (client as unknown as ClientInternal)[SYMBOL_GET_OUTPUT];
33
+ if (!output) {
34
+ throw new Error(
35
+ "Client scope contains a non-client resource. " +
36
+ "Ensure your Derived get() returns a client created with tapClientResource(), not a plain resource.",
37
+ );
38
+ }
39
+ return output.state;
40
+ };
41
+
42
+ // Global cache for function templates by field name
43
+ const fieldAccessFns = new Map<
44
+ string | symbol,
45
+ (this: ClientInternal, ...args: unknown[]) => unknown
46
+ >();
47
+
48
+ function getOrCreateProxyFn(prop: string | symbol) {
49
+ let template = fieldAccessFns.get(prop);
50
+ if (!template) {
51
+ template = function (this: ClientInternal | undefined, ...args: unknown[]) {
52
+ if (!this)
53
+ throw new Error(
54
+ `Destructuring the client method "${String(prop)}" is not supported.`,
55
+ );
56
+
57
+ const method = this[SYMBOL_GET_OUTPUT].methods[prop];
58
+ if (!method)
59
+ throw new Error(`Method "${String(prop)}" is not implemented.`);
60
+ return method(...args);
61
+ };
62
+ fieldAccessFns.set(prop, template);
63
+ }
64
+ return template;
65
+ }
66
+
67
+ class ClientProxyHandler
68
+ extends BaseProxyHandler
69
+ implements ProxyHandler<object>
70
+ {
71
+ constructor(
72
+ private readonly outputRef: {
73
+ current: ClientOutputOf<unknown, ClientMethods>;
74
+ },
75
+ private readonly index: number,
76
+ ) {
77
+ super();
78
+ }
79
+
80
+ get(_: unknown, prop: string | symbol) {
81
+ if (prop === SYMBOL_GET_OUTPUT) return this.outputRef.current;
82
+ if (prop === SYMBOL_CLIENT_INDEX) return this.index;
83
+ const introspection = handleIntrospectionProp(prop, "ClientProxy");
84
+ if (introspection !== false) return introspection;
85
+ return getOrCreateProxyFn(prop);
86
+ }
87
+
88
+ ownKeys(): ArrayLike<string | symbol> {
89
+ return Object.keys(this.outputRef.current.methods);
90
+ }
91
+
92
+ has(_: unknown, prop: string | symbol) {
93
+ if (prop === SYMBOL_GET_OUTPUT) return true;
94
+ if (prop === SYMBOL_CLIENT_INDEX) return true;
95
+ return prop in this.outputRef.current.methods;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Resource that wraps a plain resource element to create a stable client proxy.
101
+ *
102
+ * Takes a ResourceElement that returns { state, methods } and
103
+ * wraps it to produce a stable client proxy. This adds the client to the
104
+ * client stack, enabling event scoping.
105
+ *
106
+ * Use this for 1:1 client mappings where you want event scoping to work correctly.
107
+ *
108
+ * @example
109
+ * ```typescript
110
+ * const MessageResource = resource(({ messageId }: { messageId: string }) => {
111
+ * return tapInlineResource(
112
+ * tapClientResource(InnerMessageResource({ messageId }))
113
+ * );
114
+ * });
115
+ * ```
116
+ */
117
+ export const ClientResource = resource(
118
+ <TState, TMethods extends ClientMethods>(
119
+ element: ResourceElement<ClientOutputOf<TState, TMethods>>,
120
+ ): ClientOutputOf<TState, TMethods> => {
121
+ const valueRef = tapRef(
122
+ null as unknown as ClientOutputOf<TState, TMethods>,
123
+ );
124
+
125
+ const index = tapClientStack().length;
126
+ const methods = tapMemo(
127
+ () =>
128
+ new Proxy<TMethods>(
129
+ {} as TMethods,
130
+ new ClientProxyHandler(valueRef, index),
131
+ ),
132
+ [],
133
+ );
134
+
135
+ const value = tapWithClientStack(methods, () => tapResource(element));
136
+ if (!valueRef.current) {
137
+ valueRef.current = value;
138
+ }
139
+
140
+ tapEffect(() => {
141
+ valueRef.current = value;
142
+ });
143
+
144
+ return { methods, state: value.state };
145
+ },
146
+ );
147
+
148
+ export const tapClientResource = <TState, TMethods extends ClientMethods>(
149
+ element: ResourceElement<ClientOutputOf<TState, TMethods>>,
150
+ ) => {
151
+ return tapInlineResource(ClientResource(element));
152
+ };
@@ -0,0 +1,186 @@
1
+ import type { ResourceElement } from "@assistant-ui/tap";
2
+ import type {
3
+ AssistantEventName,
4
+ AssistantEventCallback,
5
+ AssistantEventSelector,
6
+ } from "./events";
7
+
8
+ /**
9
+ * Base type for methods that can be called on a client.
10
+ */
11
+ export interface ClientMethods {
12
+ [key: string | symbol]: (...args: any[]) => any;
13
+ }
14
+
15
+ type ClientMetaType = { source: ClientNames; query: Record<string, unknown> };
16
+
17
+ /**
18
+ * Schema of a client in the assistant system (internal type).
19
+ * @template TState - The state type for this client
20
+ * @template TMethods - The methods available on this client
21
+ * @template TMeta - Source/query metadata (optional)
22
+ * @template TEvents - Events that this client can emit (optional)
23
+ * @internal
24
+ */
25
+ export type ClientSchema<
26
+ TState = unknown,
27
+ TMethods extends ClientMethods = ClientMethods,
28
+ TMeta extends ClientMetaType = never,
29
+ TEvents extends Record<string, unknown> = never,
30
+ > = {
31
+ state: TState;
32
+ methods: TMethods;
33
+ meta?: TMeta;
34
+ events?: TEvents;
35
+ };
36
+
37
+ /**
38
+ * Module augmentation interface for assistant-ui store type extensions.
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * declare module "@assistant-ui/store" {
43
+ * interface ClientRegistry {
44
+ * // Simple client (meta and events are optional)
45
+ * foo: {
46
+ * state: { bar: string };
47
+ * methods: { updateBar: (bar: string) => void };
48
+ * };
49
+ * // Full client with meta and events
50
+ * bar: {
51
+ * state: { id: string };
52
+ * methods: { update: () => void };
53
+ * meta: { source: "fooList"; query: { index: number } };
54
+ * events: {
55
+ * "bar.updated": { id: string };
56
+ * };
57
+ * };
58
+ * }
59
+ * }
60
+ * ```
61
+ */
62
+ export interface ClientRegistry {}
63
+
64
+ type ClientEventsType<K extends ClientNames> = Record<
65
+ `${K}.${string}`,
66
+ unknown
67
+ >;
68
+
69
+ type ClientError<E extends string> = {
70
+ state: E;
71
+ methods: Record<E, () => E>;
72
+ meta: { source: ClientNames; query: Record<E, E> };
73
+ events: Record<`${E}.`, E>;
74
+ };
75
+
76
+ type ValidateClient<K extends keyof ClientRegistry> =
77
+ ClientRegistry[K] extends { methods: ClientMethods }
78
+ ? "meta" extends keyof ClientRegistry[K]
79
+ ? ClientRegistry[K]["meta"] extends ClientMetaType
80
+ ? "events" extends keyof ClientRegistry[K]
81
+ ? ClientRegistry[K]["events"] extends ClientEventsType<K>
82
+ ? ClientRegistry[K]
83
+ : ClientError<`ERROR: ${K & string} has invalid events type`>
84
+ : ClientRegistry[K]
85
+ : ClientError<`ERROR: ${K & string} has invalid meta type`>
86
+ : "events" extends keyof ClientRegistry[K]
87
+ ? ClientRegistry[K]["events"] extends ClientEventsType<K>
88
+ ? ClientRegistry[K]
89
+ : ClientError<`ERROR: ${K & string} has invalid events type`>
90
+ : ClientRegistry[K]
91
+ : ClientError<`ERROR: ${K & string} has invalid methods type`>;
92
+
93
+ type ClientSchemas = keyof ClientRegistry extends never
94
+ ? {
95
+ "ERROR: No clients were defined": ClientError<"ERROR: No clients were defined">;
96
+ }
97
+ : { [K in keyof ClientRegistry]: ValidateClient<K> };
98
+
99
+ /**
100
+ * Output type that client resources return with state and methods.
101
+ *
102
+ * @example
103
+ * ```typescript
104
+ * const FooResource = resource((): ClientResourceOutput<"foo"> => {
105
+ * const [state, setState] = tapState({ bar: "hello" });
106
+ * return {
107
+ * state,
108
+ * methods: {
109
+ * updateBar: (b) => setState({ bar: b })
110
+ * }
111
+ * };
112
+ * });
113
+ * ```
114
+ */
115
+ export type ClientOutput<K extends ClientNames> = ClientOutputOf<
116
+ ClientSchemas[K]["state"],
117
+ ClientSchemas[K]["methods"] & ClientMethods
118
+ >;
119
+
120
+ /**
121
+ * Generic version of ClientResourceOutput for library code.
122
+ */
123
+ export type ClientOutputOf<TState, TMethods extends ClientMethods> = {
124
+ state: TState;
125
+ methods: TMethods;
126
+ };
127
+
128
+ export type ClientNames = keyof ClientSchemas extends infer U ? U : never;
129
+
130
+ export type ClientEvents<K extends ClientNames> =
131
+ "events" extends keyof ClientSchemas[K]
132
+ ? ClientSchemas[K]["events"] extends ClientEventsType<K>
133
+ ? ClientSchemas[K]["events"]
134
+ : never
135
+ : never;
136
+
137
+ export type ClientMeta<K extends ClientNames> =
138
+ "meta" extends keyof ClientSchemas[K]
139
+ ? Pick<
140
+ ClientSchemas[K]["meta"] extends ClientMetaType
141
+ ? ClientSchemas[K]["meta"]
142
+ : never,
143
+ "source" | "query"
144
+ >
145
+ : never;
146
+
147
+ export type ClientElement<K extends ClientNames> = ResourceElement<
148
+ ClientOutput<K>
149
+ >;
150
+
151
+ /**
152
+ * Unsubscribe function type.
153
+ */
154
+ export type Unsubscribe = () => void;
155
+
156
+ /**
157
+ * State type extracted from all clients.
158
+ */
159
+ export type AssistantState = {
160
+ [K in ClientNames]: ClientSchemas[K]["state"];
161
+ };
162
+
163
+ /**
164
+ * Type for a client accessor - a function that returns the methods,
165
+ * with source/query metadata attached (derived from meta).
166
+ */
167
+ export type AssistantClientAccessor<K extends ClientNames> =
168
+ (() => ClientSchemas[K]["methods"]) &
169
+ (
170
+ | ClientMeta<K>
171
+ | { source: "root"; query: Record<string, never> }
172
+ | { source: null; query: null }
173
+ );
174
+
175
+ /**
176
+ * The assistant client type with all registered clients.
177
+ */
178
+ export type AssistantClient = {
179
+ [K in ClientNames]: AssistantClientAccessor<K>;
180
+ } & {
181
+ subscribe(listener: () => void): Unsubscribe;
182
+ on<TEvent extends AssistantEventName>(
183
+ selector: AssistantEventSelector<TEvent>,
184
+ callback: AssistantEventCallback<TEvent>,
185
+ ): Unsubscribe;
186
+ };