@assistant-ui/store 0.0.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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -0
  3. package/dist/AssistantContext.d.ts +22 -0
  4. package/dist/AssistantContext.d.ts.map +1 -0
  5. package/dist/AssistantContext.js +44 -0
  6. package/dist/AssistantContext.js.map +1 -0
  7. package/dist/DerivedScope.d.ts +18 -0
  8. package/dist/DerivedScope.d.ts.map +1 -0
  9. package/dist/DerivedScope.js +11 -0
  10. package/dist/DerivedScope.js.map +1 -0
  11. package/dist/ScopeRegistry.d.ts +41 -0
  12. package/dist/ScopeRegistry.d.ts.map +1 -0
  13. package/dist/ScopeRegistry.js +17 -0
  14. package/dist/ScopeRegistry.js.map +1 -0
  15. package/dist/asStore.d.ts +20 -0
  16. package/dist/asStore.d.ts.map +1 -0
  17. package/dist/asStore.js +23 -0
  18. package/dist/asStore.js.map +1 -0
  19. package/dist/index.d.ts +13 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +20 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/tapApi.d.ts +36 -0
  24. package/dist/tapApi.d.ts.map +1 -0
  25. package/dist/tapApi.js +52 -0
  26. package/dist/tapApi.js.map +1 -0
  27. package/dist/tapLookupResources.d.ts +44 -0
  28. package/dist/tapLookupResources.d.ts.map +1 -0
  29. package/dist/tapLookupResources.js +21 -0
  30. package/dist/tapLookupResources.js.map +1 -0
  31. package/dist/tapStoreList.d.ts +76 -0
  32. package/dist/tapStoreList.d.ts.map +1 -0
  33. package/dist/tapStoreList.js +46 -0
  34. package/dist/tapStoreList.js.map +1 -0
  35. package/dist/types.d.ts +86 -0
  36. package/dist/types.d.ts.map +1 -0
  37. package/dist/types.js +1 -0
  38. package/dist/types.js.map +1 -0
  39. package/dist/useAssistantClient.d.ts +42 -0
  40. package/dist/useAssistantClient.d.ts.map +1 -0
  41. package/dist/useAssistantClient.js +153 -0
  42. package/dist/useAssistantClient.js.map +1 -0
  43. package/dist/useAssistantState.d.ts +18 -0
  44. package/dist/useAssistantState.d.ts.map +1 -0
  45. package/dist/useAssistantState.js +53 -0
  46. package/dist/useAssistantState.js.map +1 -0
  47. package/dist/utils/splitScopes.d.ts +24 -0
  48. package/dist/utils/splitScopes.d.ts.map +1 -0
  49. package/dist/utils/splitScopes.js +18 -0
  50. package/dist/utils/splitScopes.js.map +1 -0
  51. package/package.json +50 -0
  52. package/src/AssistantContext.tsx +64 -0
  53. package/src/DerivedScope.ts +21 -0
  54. package/src/ScopeRegistry.ts +58 -0
  55. package/src/asStore.ts +40 -0
  56. package/src/index.ts +13 -0
  57. package/src/tapApi.ts +91 -0
  58. package/src/tapLookupResources.ts +62 -0
  59. package/src/tapStoreList.ts +133 -0
  60. package/src/types.ts +120 -0
  61. package/src/useAssistantClient.tsx +250 -0
  62. package/src/useAssistantState.tsx +80 -0
  63. package/src/utils/splitScopes.ts +38 -0
package/src/types.ts ADDED
@@ -0,0 +1,120 @@
1
+ import type { ResourceElement } from "@assistant-ui/tap";
2
+
3
+ /**
4
+ * Definition of a scope in the assistant client (internal type)
5
+ * @template TValue - The API type (must include getState() and any actions)
6
+ * @template TSource - The parent scope name (or "root" for top-level scopes)
7
+ * @template TQuery - The query parameters needed to access this scope from its source
8
+ * @internal
9
+ */
10
+ export type ScopeDefinition<
11
+ TValue = any,
12
+ TSource extends string | "root" = any,
13
+ TQuery = any,
14
+ > = {
15
+ value: TValue;
16
+ source: TSource;
17
+ query: TQuery;
18
+ };
19
+
20
+ /**
21
+ * Module augmentation interface for assistant-ui store type extensions.
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * declare module "@assistant-ui/store" {
26
+ * interface AssistantScopeRegistry {
27
+ * foo: {
28
+ * value: { getState: () => { bar: string }; updateBar: (bar: string) => void };
29
+ * source: "root";
30
+ * query: Record<string, never>;
31
+ * };
32
+ * }
33
+ * }
34
+ * ```
35
+ */
36
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
37
+ export interface AssistantScopeRegistry {}
38
+
39
+ export type AssistantScopes = keyof AssistantScopeRegistry extends never
40
+ ? Record<"ERROR: No scopes were defined", ScopeDefinition>
41
+ : { [K in keyof AssistantScopeRegistry]: AssistantScopeRegistry[K] };
42
+
43
+ /**
44
+ * Helper type to extract the value type from a scope definition
45
+ */
46
+ export type ScopeValue<T extends ScopeDefinition> = T["value"];
47
+
48
+ /**
49
+ * Helper type to extract the source type from a scope definition
50
+ */
51
+ export type ScopeSource<T extends ScopeDefinition> = T["source"];
52
+
53
+ /**
54
+ * Helper type to extract the query type from a scope definition
55
+ */
56
+ export type ScopeQuery<T extends ScopeDefinition> = T["query"];
57
+
58
+ /**
59
+ * Type for a scope field - a function that returns the current API value,
60
+ * with source and query metadata attached
61
+ */
62
+ export type ScopeField<T extends ScopeDefinition> = (() => ScopeValue<T>) &
63
+ (
64
+ | {
65
+ source: ScopeSource<T>;
66
+ query: ScopeQuery<T>;
67
+ }
68
+ | {
69
+ source: null;
70
+ query: null;
71
+ }
72
+ );
73
+
74
+ /**
75
+ * Props passed to a derived scope resource element
76
+ */
77
+ export type DerivedScopeProps<T extends ScopeDefinition> = {
78
+ get: (parent: AssistantClient) => ScopeValue<T>;
79
+ source: ScopeSource<T>;
80
+ query: ScopeQuery<T>;
81
+ };
82
+
83
+ /**
84
+ * Input type for scope definitions - ResourceElement that returns the API value
85
+ * Can optionally include source/query metadata via DerivedScope
86
+ */
87
+ export type ScopeInput<T extends ScopeDefinition> = ResourceElement<{
88
+ api: ScopeValue<T>;
89
+ }>;
90
+
91
+ /**
92
+ * Map of scope names to their input definitions
93
+ */
94
+ export type ScopesInput = {
95
+ [K in keyof AssistantScopes]?: ScopeInput<AssistantScopes[K]>;
96
+ };
97
+
98
+ /**
99
+ * Unsubscribe function type
100
+ */
101
+ export type Unsubscribe = () => void;
102
+
103
+ /**
104
+ * State type extracted from all scopes
105
+ */
106
+ export type AssistantState = {
107
+ [K in keyof AssistantScopes]: ReturnType<
108
+ AssistantScopes[K]["value"]["getState"]
109
+ >;
110
+ };
111
+
112
+ /**
113
+ * The assistant client type with all registered scopes
114
+ */
115
+ export type AssistantClient = {
116
+ [K in keyof AssistantScopes]: ScopeField<AssistantScopes[K]>;
117
+ } & {
118
+ subscribe(listener: () => void): Unsubscribe;
119
+ flushSync(): void;
120
+ };
@@ -0,0 +1,250 @@
1
+ import { useMemo } from "react";
2
+ import { useResource } from "@assistant-ui/tap/react";
3
+ import {
4
+ resource,
5
+ tapMemo,
6
+ tapResource,
7
+ tapResources,
8
+ tapEffectEvent,
9
+ ResourceElement,
10
+ } from "@assistant-ui/tap";
11
+ import type {
12
+ AssistantClient,
13
+ AssistantScopes,
14
+ ScopesInput,
15
+ ScopeField,
16
+ ScopeInput,
17
+ DerivedScopeProps,
18
+ } from "./types";
19
+ import { asStore } from "./asStore";
20
+ import { useAssistantContextValue } from "./AssistantContext";
21
+ import { splitScopes } from "./utils/splitScopes";
22
+
23
+ /**
24
+ * Resource for a single root scope
25
+ * Returns a tuple of [scopeName, {scopeFunction, subscribe, flushSync}]
26
+ */
27
+ const RootScopeResource = resource(
28
+ <K extends keyof AssistantScopes>({
29
+ scopeName,
30
+ element,
31
+ }: {
32
+ scopeName: K;
33
+ element: ScopeInput<AssistantScopes[K]>;
34
+ }) => {
35
+ const store = tapResource(asStore(element));
36
+
37
+ return tapMemo(() => {
38
+ const scopeFunction = (() => store.getState().api) as ScopeField<
39
+ AssistantScopes[K]
40
+ >;
41
+ scopeFunction.source = "root";
42
+ scopeFunction.query = {} as AssistantScopes[K]["query"];
43
+
44
+ return [
45
+ scopeName,
46
+ {
47
+ scopeFunction,
48
+ subscribe: store.subscribe,
49
+ flushSync: store.flushSync,
50
+ },
51
+ ] as const;
52
+ }, [scopeName, store]);
53
+ },
54
+ );
55
+
56
+ /**
57
+ * Resource for all root scopes
58
+ * Mounts each root scope and returns an object mapping scope names to their stores
59
+ */
60
+ const RootScopesResource = resource((scopes: ScopesInput) => {
61
+ const resultEntries = tapResources(
62
+ Object.entries(scopes).map(([scopeName, element]) =>
63
+ RootScopeResource(
64
+ {
65
+ scopeName: scopeName as keyof AssistantScopes,
66
+ element: element as ScopeInput<
67
+ AssistantScopes[keyof AssistantScopes]
68
+ >,
69
+ },
70
+ { key: scopeName },
71
+ ),
72
+ ),
73
+ );
74
+
75
+ return tapMemo(() => {
76
+ if (resultEntries.length === 0) {
77
+ return {
78
+ scopes: {},
79
+ };
80
+ }
81
+
82
+ return {
83
+ scopes: Object.fromEntries(
84
+ resultEntries.map(([scopeName, { scopeFunction }]) => [
85
+ scopeName,
86
+ scopeFunction,
87
+ ]),
88
+ ) as {
89
+ [K in keyof typeof scopes]: ScopeField<AssistantScopes[K]>;
90
+ },
91
+ subscribe: (callback: () => void) => {
92
+ const unsubscribes = resultEntries.map(([, { subscribe }]) => {
93
+ return subscribe(() => {
94
+ console.log("Callback called for");
95
+ callback();
96
+ });
97
+ });
98
+ return () => {
99
+ unsubscribes.forEach((unsubscribe) => unsubscribe());
100
+ };
101
+ },
102
+ flushSync: () => {
103
+ resultEntries.forEach(([, { flushSync }]) => {
104
+ flushSync();
105
+ });
106
+ },
107
+ };
108
+ }, [...resultEntries]);
109
+ });
110
+
111
+ /**
112
+ * Hook to mount and access root scopes
113
+ */
114
+ export const useRootScopes = (rootScopes: ScopesInput) => {
115
+ return useResource(RootScopesResource(rootScopes));
116
+ };
117
+
118
+ /**
119
+ * Resource for a single derived scope
120
+ * Returns a tuple of [scopeName, scopeFunction] where scopeFunction has source and query
121
+ */
122
+ const DerivedScopeResource = resource(
123
+ <K extends keyof AssistantScopes>({
124
+ scopeName,
125
+ element,
126
+ parentClient,
127
+ }: {
128
+ scopeName: K;
129
+ element: ResourceElement<
130
+ AssistantScopes[K],
131
+ DerivedScopeProps<AssistantScopes[K]>
132
+ >;
133
+ parentClient: AssistantClient;
134
+ }) => {
135
+ const get = tapEffectEvent(element.props.get);
136
+ const source = element.props.source;
137
+ const query = element.props.query;
138
+ return tapMemo(() => {
139
+ const scopeFunction = (() => get(parentClient)) as ScopeField<
140
+ AssistantScopes[K]
141
+ >;
142
+ scopeFunction.source = source;
143
+ scopeFunction.query = query;
144
+
145
+ return [scopeName, scopeFunction] as const;
146
+ }, [scopeName, get, source, JSON.stringify(query), parentClient]);
147
+ },
148
+ );
149
+
150
+ /**
151
+ * Resource for all derived scopes
152
+ * Builds stable scope functions with source and query metadata
153
+ */
154
+ const DerivedScopesResource = resource(
155
+ ({
156
+ scopes,
157
+ parentClient,
158
+ }: {
159
+ scopes: ScopesInput;
160
+ parentClient: AssistantClient;
161
+ }) => {
162
+ const resultEntries = tapResources(
163
+ Object.entries(scopes).map(([scopeName, element]) =>
164
+ DerivedScopeResource(
165
+ {
166
+ scopeName: scopeName as keyof AssistantScopes,
167
+ element: element as ScopeInput<
168
+ AssistantScopes[keyof AssistantScopes]
169
+ >,
170
+ parentClient,
171
+ },
172
+ { key: scopeName },
173
+ ),
174
+ ),
175
+ );
176
+
177
+ return tapMemo(() => {
178
+ return Object.fromEntries(resultEntries) as {
179
+ [K in keyof typeof scopes]: ScopeField<AssistantScopes[K]>;
180
+ };
181
+ }, [...resultEntries]);
182
+ },
183
+ );
184
+
185
+ /**
186
+ * Hook to mount and access derived scopes
187
+ */
188
+ export const useDerivedScopes = (
189
+ derivedScopes: ScopesInput,
190
+ parentClient: AssistantClient,
191
+ ) => {
192
+ return useResource(
193
+ DerivedScopesResource({ scopes: derivedScopes, parentClient }),
194
+ );
195
+ };
196
+
197
+ const useExtendedAssistantClientImpl = (
198
+ scopes: ScopesInput,
199
+ ): AssistantClient => {
200
+ const baseClient = useAssistantContextValue();
201
+ const { rootScopes, derivedScopes } = splitScopes(scopes);
202
+
203
+ // Mount the scopes to keep them alive
204
+ const rootFields = useRootScopes(rootScopes);
205
+ const derivedFields = useDerivedScopes(derivedScopes, baseClient);
206
+
207
+ return useMemo(() => {
208
+ // Merge base client with extended client
209
+ // If baseClient is the default proxy, spreading it will be a no-op
210
+ return {
211
+ ...baseClient,
212
+ ...rootFields.scopes,
213
+ ...derivedFields,
214
+ subscribe: rootFields.subscribe ?? baseClient.subscribe,
215
+ flushSync: rootFields.flushSync ?? baseClient.flushSync,
216
+ } as AssistantClient;
217
+ }, [baseClient, rootFields, derivedFields]);
218
+ };
219
+
220
+ /**
221
+ * Hook to access or extend the AssistantClient
222
+ *
223
+ * @example Without config - returns the client from context:
224
+ * ```typescript
225
+ * const client = useAssistantClient();
226
+ * const fooState = client.foo.getState();
227
+ * ```
228
+ *
229
+ * @example With config - creates a new client with additional scopes:
230
+ * ```typescript
231
+ * const client = useAssistantClient({
232
+ * message: DerivedScope({
233
+ * source: "thread",
234
+ * query: { type: "index", index: 0 },
235
+ * get: () => messageApi,
236
+ * }),
237
+ * });
238
+ * ```
239
+ */
240
+ export function useAssistantClient(): AssistantClient;
241
+ export function useAssistantClient(scopes: ScopesInput): AssistantClient;
242
+ export function useAssistantClient(scopes?: ScopesInput): AssistantClient {
243
+ if (scopes) {
244
+ // eslint-disable-next-line react-hooks/rules-of-hooks
245
+ return useExtendedAssistantClientImpl(scopes);
246
+ } else {
247
+ // eslint-disable-next-line react-hooks/rules-of-hooks
248
+ return useAssistantContextValue();
249
+ }
250
+ }
@@ -0,0 +1,80 @@
1
+ import { useMemo, useSyncExternalStore, useDebugValue } from "react";
2
+ import type { AssistantClient, AssistantState } from "./types";
3
+ import { useAssistantClient } from "./useAssistantClient";
4
+
5
+ /**
6
+ * Proxied state that lazily accesses scope states
7
+ */
8
+ class ProxiedAssistantState {
9
+ #client: AssistantClient;
10
+
11
+ constructor(client: AssistantClient) {
12
+ this.#client = client;
13
+ }
14
+
15
+ #getScope<K extends keyof AssistantState>(key: K): AssistantState[K] {
16
+ const scopeField = this.#client[key];
17
+ if (!scopeField) {
18
+ throw new Error(`Scope "${String(key)}" not found in client`);
19
+ }
20
+
21
+ const api = scopeField();
22
+ const state = api.getState();
23
+ return state as AssistantState[K];
24
+ }
25
+
26
+ // Create a Proxy to dynamically handle property access
27
+ static create(client: AssistantClient): AssistantState {
28
+ const instance = new ProxiedAssistantState(client);
29
+ return new Proxy(instance, {
30
+ get(target, prop) {
31
+ if (typeof prop === "string" && prop in client) {
32
+ return target.#getScope(prop as keyof AssistantState);
33
+ }
34
+ return undefined;
35
+ },
36
+ }) as unknown as AssistantState;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Hook to access a slice of the assistant state with automatic subscription
42
+ *
43
+ * @param selector - Function to select a slice of the state
44
+ * @returns The selected state slice
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * const client = useAssistantClient({
49
+ * foo: RootScope({ ... }),
50
+ * });
51
+ *
52
+ * const bar = useAssistantState((state) => state.foo.bar);
53
+ * ```
54
+ */
55
+ export const useAssistantState = <T,>(
56
+ selector: (state: AssistantState) => T,
57
+ ): T => {
58
+ const client = useAssistantClient();
59
+
60
+ const proxiedState = useMemo(
61
+ () => ProxiedAssistantState.create(client),
62
+ [client],
63
+ );
64
+
65
+ const slice = useSyncExternalStore(
66
+ client.subscribe,
67
+ () => selector(proxiedState),
68
+ () => selector(proxiedState),
69
+ );
70
+
71
+ useDebugValue(slice);
72
+
73
+ if (slice instanceof ProxiedAssistantState) {
74
+ throw new Error(
75
+ "You tried to return the entire AssistantState. This is not supported due to technical limitations.",
76
+ );
77
+ }
78
+
79
+ return slice;
80
+ };
@@ -0,0 +1,38 @@
1
+ import { DerivedScope } from "../DerivedScope";
2
+ import type { AssistantScopes, ScopeInput, ScopesInput } from "../types";
3
+
4
+ /**
5
+ * Splits a scopes object into root scopes and derived scopes.
6
+ *
7
+ * @param scopes - The scopes input object to split
8
+ * @returns An object with { rootScopes, derivedScopes }
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * const scopes = {
13
+ * foo: RootScope({ ... }),
14
+ * bar: DerivedScope({ ... }),
15
+ * };
16
+ *
17
+ * const { rootScopes, derivedScopes } = splitScopes(scopes);
18
+ * // rootScopes = { foo: ... }
19
+ * // derivedScopes = { bar: ... }
20
+ * ```
21
+ */
22
+ export function splitScopes(scopes: ScopesInput) {
23
+ const rootScopes: ScopesInput = {};
24
+ const derivedScopes: ScopesInput = {};
25
+
26
+ for (const [key, scopeElement] of Object.entries(scopes) as [
27
+ keyof ScopesInput,
28
+ ScopeInput<AssistantScopes[keyof ScopesInput]>,
29
+ ][]) {
30
+ if (scopeElement.type === DerivedScope) {
31
+ derivedScopes[key] = scopeElement;
32
+ } else {
33
+ rootScopes[key] = scopeElement;
34
+ }
35
+ }
36
+
37
+ return { rootScopes, derivedScopes };
38
+ }