@assistant-ui/store 0.0.0 → 0.0.2

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 (42) hide show
  1. package/dist/AssistantContext.js +1 -0
  2. package/dist/AssistantContext.js.map +1 -1
  3. package/dist/AssistantIf.d.ts +12 -0
  4. package/dist/AssistantIf.d.ts.map +1 -0
  5. package/dist/AssistantIf.js +16 -0
  6. package/dist/AssistantIf.js.map +1 -0
  7. package/dist/DerivedScope.d.ts +8 -6
  8. package/dist/DerivedScope.d.ts.map +1 -1
  9. package/dist/DerivedScope.js +2 -2
  10. package/dist/DerivedScope.js.map +1 -1
  11. package/dist/EventContext.d.ts +61 -0
  12. package/dist/EventContext.d.ts.map +1 -0
  13. package/dist/EventContext.js +62 -0
  14. package/dist/EventContext.js.map +1 -0
  15. package/dist/StoreContext.d.ts +9 -0
  16. package/dist/StoreContext.d.ts.map +1 -0
  17. package/dist/StoreContext.js +20 -0
  18. package/dist/StoreContext.js.map +1 -0
  19. package/dist/index.d.ts +7 -6
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +6 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/types.d.ts +21 -23
  24. package/dist/types.d.ts.map +1 -1
  25. package/dist/useAssistantClient.d.ts +10 -2
  26. package/dist/useAssistantClient.d.ts.map +1 -1
  27. package/dist/useAssistantClient.js +82 -48
  28. package/dist/useAssistantClient.js.map +1 -1
  29. package/dist/useAssistantEvent.d.ts +3 -0
  30. package/dist/useAssistantEvent.d.ts.map +1 -0
  31. package/dist/useAssistantEvent.js +17 -0
  32. package/dist/useAssistantEvent.js.map +1 -0
  33. package/package.json +5 -6
  34. package/src/AssistantContext.tsx +1 -1
  35. package/src/AssistantIf.tsx +25 -0
  36. package/src/DerivedScope.ts +9 -7
  37. package/src/EventContext.ts +187 -0
  38. package/src/StoreContext.ts +28 -0
  39. package/src/index.ts +33 -6
  40. package/src/types.ts +41 -42
  41. package/src/useAssistantClient.tsx +106 -53
  42. package/src/useAssistantEvent.ts +22 -0
package/src/types.ts CHANGED
@@ -1,20 +1,30 @@
1
1
  import type { ResourceElement } from "@assistant-ui/tap";
2
+ import type {
3
+ AssistantEvent,
4
+ AssistantEventCallback,
5
+ AssistantEventSelector,
6
+ } from "./EventContext";
7
+
8
+ type ScopeValueType = Record<string, unknown> & {
9
+ getState: () => Record<string, unknown>;
10
+ };
11
+ type ScopeMetaType = { source: string; query: Record<string, unknown> };
2
12
 
3
13
  /**
4
14
  * Definition of a scope in the assistant client (internal type)
5
15
  * @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
16
+ * @template TMeta - Source/query metadata (use ScopeMeta or discriminated union)
17
+ * @template TEvents - Optional events that this scope can emit
8
18
  * @internal
9
19
  */
10
20
  export type ScopeDefinition<
11
- TValue = any,
12
- TSource extends string | "root" = any,
13
- TQuery = any,
21
+ TValue extends ScopeValueType = ScopeValueType,
22
+ TMeta extends ScopeMetaType = ScopeMetaType,
23
+ TEvents extends Record<string, unknown> = Record<string, unknown>,
14
24
  > = {
15
25
  value: TValue;
16
- source: TSource;
17
- query: TQuery;
26
+ meta: TMeta;
27
+ events: TEvents;
18
28
  };
19
29
 
20
30
  /**
@@ -26,58 +36,43 @@ export type ScopeDefinition<
26
36
  * interface AssistantScopeRegistry {
27
37
  * foo: {
28
38
  * value: { getState: () => { bar: string }; updateBar: (bar: string) => void };
29
- * source: "root";
30
- * query: Record<string, never>;
39
+ * meta: { source: "root"; query: Record<string, never> };
40
+ * events: {
41
+ * "foo.updated": { id: string; newValue: string };
42
+ * "foo.deleted": { id: string };
43
+ * };
44
+ * };
45
+ * // Example with multiple sources (discriminated union):
46
+ * bar: {
47
+ * value: { getState: () => { id: string } };
48
+ * meta:
49
+ * | { source: "fooList"; query: { index: number } }
50
+ * | { source: "barList"; query: { id: string } };
51
+ * events: Record<string, never>;
31
52
  * };
32
53
  * }
33
54
  * }
34
55
  * ```
35
56
  */
36
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type
37
57
  export interface AssistantScopeRegistry {}
38
58
 
39
59
  export type AssistantScopes = keyof AssistantScopeRegistry extends never
40
60
  ? Record<"ERROR: No scopes were defined", ScopeDefinition>
41
61
  : { [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
62
  /**
59
63
  * Type for a scope field - a function that returns the current API value,
60
- * with source and query metadata attached
64
+ * with source/query metadata attached (derived from meta)
61
65
  */
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
- );
66
+ export type ScopeField<T extends ScopeDefinition> = (() => T["value"]) &
67
+ (T["meta"] | { source: null; query: null });
73
68
 
74
69
  /**
75
70
  * Props passed to a derived scope resource element
76
71
  */
77
72
  export type DerivedScopeProps<T extends ScopeDefinition> = {
78
- get: (parent: AssistantClient) => ScopeValue<T>;
79
- source: ScopeSource<T>;
80
- query: ScopeQuery<T>;
73
+ get: (parent: AssistantClient) => T["value"];
74
+ source: T["meta"]["source"];
75
+ query: T["meta"]["query"];
81
76
  };
82
77
 
83
78
  /**
@@ -85,7 +80,7 @@ export type DerivedScopeProps<T extends ScopeDefinition> = {
85
80
  * Can optionally include source/query metadata via DerivedScope
86
81
  */
87
82
  export type ScopeInput<T extends ScopeDefinition> = ResourceElement<{
88
- api: ScopeValue<T>;
83
+ api: T["value"];
89
84
  }>;
90
85
 
91
86
  /**
@@ -117,4 +112,8 @@ export type AssistantClient = {
117
112
  } & {
118
113
  subscribe(listener: () => void): Unsubscribe;
119
114
  flushSync(): void;
115
+ on<TEvent extends AssistantEvent>(
116
+ selector: AssistantEventSelector<TEvent>,
117
+ callback: AssistantEventCallback<TEvent>,
118
+ ): Unsubscribe;
120
119
  };
@@ -6,6 +6,7 @@ import {
6
6
  tapResource,
7
7
  tapResources,
8
8
  tapEffectEvent,
9
+ tapInlineResource,
9
10
  ResourceElement,
10
11
  } from "@assistant-ui/tap";
11
12
  import type {
@@ -19,6 +20,34 @@ import type {
19
20
  import { asStore } from "./asStore";
20
21
  import { useAssistantContextValue } from "./AssistantContext";
21
22
  import { splitScopes } from "./utils/splitScopes";
23
+ import {
24
+ EventManager,
25
+ normalizeEventSelector,
26
+ type AssistantEvent,
27
+ type AssistantEventCallback,
28
+ type AssistantEventSelector,
29
+ } from "./EventContext";
30
+ import { withStoreContextProvider } from "./StoreContext";
31
+
32
+ /**
33
+ * Resource that renders a store with the store context provider.
34
+ * This ensures the context is re-established on every re-render.
35
+ */
36
+ const RootScopeStoreResource = resource(
37
+ <K extends keyof AssistantScopes>({
38
+ element,
39
+ events,
40
+ parent,
41
+ }: {
42
+ element: ScopeInput<AssistantScopes[K]>;
43
+ events: EventManager;
44
+ parent: AssistantClient;
45
+ }) => {
46
+ return withStoreContextProvider({ events, parent }, () =>
47
+ tapInlineResource(element),
48
+ );
49
+ },
50
+ );
22
51
 
23
52
  /**
24
53
  * Resource for a single root scope
@@ -28,18 +57,24 @@ const RootScopeResource = resource(
28
57
  <K extends keyof AssistantScopes>({
29
58
  scopeName,
30
59
  element,
60
+ events,
61
+ parent,
31
62
  }: {
32
63
  scopeName: K;
33
64
  element: ScopeInput<AssistantScopes[K]>;
65
+ events: EventManager;
66
+ parent: AssistantClient;
34
67
  }) => {
35
- const store = tapResource(asStore(element));
68
+ const store = tapResource(
69
+ asStore(RootScopeStoreResource({ element, events, parent })),
70
+ );
36
71
 
37
72
  return tapMemo(() => {
38
73
  const scopeFunction = (() => store.getState().api) as ScopeField<
39
74
  AssistantScopes[K]
40
75
  >;
41
76
  scopeFunction.source = "root";
42
- scopeFunction.query = {} as AssistantScopes[K]["query"];
77
+ scopeFunction.query = {};
43
78
 
44
79
  return [
45
80
  scopeName,
@@ -57,62 +92,81 @@ const RootScopeResource = resource(
57
92
  * Resource for all root scopes
58
93
  * Mounts each root scope and returns an object mapping scope names to their stores
59
94
  */
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 },
95
+ const RootScopesResource = resource(
96
+ ({ scopes, parent }: { scopes: ScopesInput; parent: AssistantClient }) => {
97
+ const events = tapInlineResource(EventManager());
98
+
99
+ const resultEntries = tapResources(
100
+ Object.entries(scopes).map(([scopeName, element]) =>
101
+ RootScopeResource(
102
+ {
103
+ scopeName: scopeName as keyof AssistantScopes,
104
+ element: element as ScopeInput<
105
+ AssistantScopes[keyof AssistantScopes]
106
+ >,
107
+ events,
108
+ parent,
109
+ },
110
+ { key: scopeName },
111
+ ),
71
112
  ),
72
- ),
73
- );
113
+ );
74
114
 
75
- return tapMemo(() => {
76
- if (resultEntries.length === 0) {
77
- return {
78
- scopes: {},
79
- };
80
- }
115
+ const on = <TEvent extends AssistantEvent>(
116
+ selector: AssistantEventSelector<TEvent>,
117
+ callback: AssistantEventCallback<TEvent>,
118
+ ) => {
119
+ const { event } = normalizeEventSelector(selector);
120
+ return events.on(event, callback);
121
+ };
81
122
 
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());
123
+ return tapMemo(() => {
124
+ if (resultEntries.length === 0) {
125
+ return {
126
+ scopes: {},
127
+ on,
100
128
  };
101
- },
102
- flushSync: () => {
103
- resultEntries.forEach(([, { flushSync }]) => {
104
- flushSync();
105
- });
106
- },
107
- };
108
- }, [...resultEntries]);
109
- });
129
+ }
130
+
131
+ return {
132
+ scopes: Object.fromEntries(
133
+ resultEntries.map(([scopeName, { scopeFunction }]) => [
134
+ scopeName,
135
+ scopeFunction,
136
+ ]),
137
+ ) as {
138
+ [K in keyof typeof scopes]: ScopeField<AssistantScopes[K]>;
139
+ },
140
+ subscribe: (callback: () => void) => {
141
+ const unsubscribes = resultEntries.map(([, { subscribe }]) => {
142
+ return subscribe(() => {
143
+ console.log("Callback called for");
144
+ callback();
145
+ });
146
+ });
147
+ return () => {
148
+ unsubscribes.forEach((unsubscribe) => unsubscribe());
149
+ };
150
+ },
151
+ flushSync: () => {
152
+ resultEntries.forEach(([, { flushSync }]) => {
153
+ flushSync();
154
+ });
155
+ },
156
+ on,
157
+ };
158
+ }, [...resultEntries, events]);
159
+ },
160
+ );
110
161
 
111
162
  /**
112
163
  * Hook to mount and access root scopes
113
164
  */
114
- export const useRootScopes = (rootScopes: ScopesInput) => {
115
- return useResource(RootScopesResource(rootScopes));
165
+ export const useRootScopes = (
166
+ rootScopes: ScopesInput,
167
+ parent: AssistantClient,
168
+ ) => {
169
+ return useResource(RootScopesResource({ scopes: rootScopes, parent }));
116
170
  };
117
171
 
118
172
  /**
@@ -201,7 +255,7 @@ const useExtendedAssistantClientImpl = (
201
255
  const { rootScopes, derivedScopes } = splitScopes(scopes);
202
256
 
203
257
  // Mount the scopes to keep them alive
204
- const rootFields = useRootScopes(rootScopes);
258
+ const rootFields = useRootScopes(rootScopes, baseClient);
205
259
  const derivedFields = useDerivedScopes(derivedScopes, baseClient);
206
260
 
207
261
  return useMemo(() => {
@@ -213,6 +267,7 @@ const useExtendedAssistantClientImpl = (
213
267
  ...derivedFields,
214
268
  subscribe: rootFields.subscribe ?? baseClient.subscribe,
215
269
  flushSync: rootFields.flushSync ?? baseClient.flushSync,
270
+ on: rootFields.on ?? baseClient.on,
216
271
  } as AssistantClient;
217
272
  }, [baseClient, rootFields, derivedFields]);
218
273
  };
@@ -241,10 +296,8 @@ export function useAssistantClient(): AssistantClient;
241
296
  export function useAssistantClient(scopes: ScopesInput): AssistantClient;
242
297
  export function useAssistantClient(scopes?: ScopesInput): AssistantClient {
243
298
  if (scopes) {
244
- // eslint-disable-next-line react-hooks/rules-of-hooks
245
299
  return useExtendedAssistantClientImpl(scopes);
246
300
  } else {
247
- // eslint-disable-next-line react-hooks/rules-of-hooks
248
301
  return useAssistantContextValue();
249
302
  }
250
303
  }
@@ -0,0 +1,22 @@
1
+ import { useEffect, useEffectEvent } from "react";
2
+ import { useAssistantClient } from "./useAssistantClient";
3
+ import type {
4
+ AssistantEvent,
5
+ AssistantEventCallback,
6
+ AssistantEventSelector,
7
+ } from "./EventContext";
8
+ import { normalizeEventSelector } from "./EventContext";
9
+
10
+ export const useAssistantEvent = <TEvent extends AssistantEvent>(
11
+ selector: AssistantEventSelector<TEvent>,
12
+ callback: AssistantEventCallback<TEvent>,
13
+ ) => {
14
+ const client = useAssistantClient();
15
+ const callbackRef = useEffectEvent(callback);
16
+
17
+ const { scope, event } = normalizeEventSelector(selector);
18
+ useEffect(
19
+ () => client.on({ scope, event }, callbackRef),
20
+ [client, scope, event],
21
+ );
22
+ };