cogsbox-state 0.5.472 → 0.5.474

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 (46) hide show
  1. package/README.md +48 -18
  2. package/dist/CogsState.d.ts +98 -82
  3. package/dist/CogsState.d.ts.map +1 -1
  4. package/dist/CogsState.jsx +1030 -960
  5. package/dist/CogsState.jsx.map +1 -1
  6. package/dist/Components.d.ts.map +1 -1
  7. package/dist/Components.jsx +299 -219
  8. package/dist/Components.jsx.map +1 -1
  9. package/dist/PluginRunner.d.ts +10 -0
  10. package/dist/PluginRunner.d.ts.map +1 -0
  11. package/dist/PluginRunner.jsx +122 -0
  12. package/dist/PluginRunner.jsx.map +1 -0
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +33 -26
  16. package/dist/index.js.map +1 -1
  17. package/dist/pluginStore.d.ts +81 -0
  18. package/dist/pluginStore.d.ts.map +1 -0
  19. package/dist/pluginStore.js +52 -0
  20. package/dist/pluginStore.js.map +1 -0
  21. package/dist/plugins.d.ts +1323 -0
  22. package/dist/plugins.d.ts.map +1 -0
  23. package/dist/plugins.js +76 -0
  24. package/dist/plugins.js.map +1 -0
  25. package/dist/store.d.ts +50 -15
  26. package/dist/store.d.ts.map +1 -1
  27. package/dist/store.js +509 -470
  28. package/dist/store.js.map +1 -1
  29. package/dist/utility.d.ts +1 -1
  30. package/dist/utility.d.ts.map +1 -1
  31. package/dist/utility.js +12 -12
  32. package/dist/utility.js.map +1 -1
  33. package/dist/validation.d.ts +7 -0
  34. package/dist/validation.d.ts.map +1 -0
  35. package/dist/validation.js +39 -0
  36. package/dist/validation.js.map +1 -0
  37. package/package.json +13 -3
  38. package/src/CogsState.tsx +657 -457
  39. package/src/Components.tsx +291 -194
  40. package/src/PluginRunner.tsx +203 -0
  41. package/src/index.ts +2 -0
  42. package/src/pluginStore.ts +176 -0
  43. package/src/plugins.ts +544 -0
  44. package/src/store.ts +748 -493
  45. package/src/utility.ts +31 -31
  46. package/src/validation.ts +84 -0
@@ -0,0 +1,203 @@
1
+ import React, { useEffect, useMemo, useState, useRef, useReducer } from 'react';
2
+ import { ClientActivityEvent, pluginStore } from './pluginStore';
3
+ import { isDeepEqual } from './utility';
4
+ import { createMetadataContext, toDeconstructedMethods } from './plugins';
5
+ import type { CogsPlugin } from './plugins';
6
+ import type { StateObject, UpdateTypeDetail } from './CogsState';
7
+ import { ClientActivityState, FormEventType } from './store';
8
+
9
+ const { setHookResult, removeHookResult } = pluginStore.getState();
10
+
11
+ const PluginInstance = React.memo(
12
+ ({
13
+ stateKey,
14
+ plugin,
15
+ options,
16
+ stateHandler,
17
+ }: {
18
+ stateKey: string;
19
+ plugin: CogsPlugin<any, any, any, any, any>;
20
+ options: any;
21
+ stateHandler: StateObject<any>;
22
+ }) => {
23
+ const [isInitialMount, setIsInitialMount] = useState(true);
24
+ const metadataContext = useMemo(
25
+ () => createMetadataContext(stateKey, plugin.name),
26
+ [stateKey, plugin.name]
27
+ );
28
+
29
+ const deconstructed = useMemo(
30
+ () => toDeconstructedMethods(stateHandler),
31
+ [stateHandler]
32
+ );
33
+
34
+ const hookContext = useMemo(
35
+ () => ({
36
+ stateKey,
37
+ pluginName: plugin.name,
38
+ isInitialMount,
39
+ options,
40
+ ...deconstructed,
41
+ ...metadataContext,
42
+ }),
43
+ [
44
+ stateKey,
45
+ plugin.name,
46
+ isInitialMount,
47
+ options,
48
+ deconstructed,
49
+ metadataContext,
50
+ ]
51
+ );
52
+
53
+ const hookData = plugin.useHook ? plugin.useHook(hookContext) : undefined;
54
+
55
+ useEffect(() => {
56
+ setIsInitialMount(false);
57
+ }, []);
58
+
59
+ useEffect(() => {
60
+ if (plugin.useHook) setHookResult(stateKey, plugin.name, hookData);
61
+ else removeHookResult(stateKey, plugin.name);
62
+ return () => removeHookResult(stateKey, plugin.name);
63
+ }, [stateKey, plugin.name, !!plugin.useHook, hookData]);
64
+
65
+ const lastProcessedOptionsRef = useRef<any>();
66
+ const [isInitialTransform, setIsInitialTransform] = useState(true);
67
+
68
+ useEffect(() => {
69
+ if (plugin.transformState) {
70
+ if (!isDeepEqual(options, lastProcessedOptionsRef.current)) {
71
+ plugin.transformState({
72
+ stateKey,
73
+ pluginName: plugin.name,
74
+ options,
75
+ hookData,
76
+ isInitialTransform,
77
+ ...deconstructed,
78
+ ...metadataContext,
79
+ });
80
+ lastProcessedOptionsRef.current = options;
81
+ setIsInitialTransform(false);
82
+ }
83
+ }
84
+ }, [
85
+ stateKey,
86
+ plugin,
87
+ options,
88
+ hookData,
89
+ isInitialTransform,
90
+ deconstructed,
91
+ metadataContext,
92
+ ]);
93
+
94
+ const hookDataRef = useRef(hookData);
95
+ hookDataRef.current = hookData;
96
+
97
+ useEffect(() => {
98
+ if (!plugin.onUpdate) return;
99
+
100
+ const handleUpdate = (update: UpdateTypeDetail) => {
101
+ if (update.stateKey === stateKey) {
102
+ plugin.onUpdate!({
103
+ stateKey,
104
+ pluginName: plugin.name,
105
+ update,
106
+ path: update.path,
107
+ options,
108
+ hookData: hookDataRef.current,
109
+ ...deconstructed,
110
+ ...metadataContext,
111
+ });
112
+ }
113
+ };
114
+
115
+ const unsubscribe = pluginStore
116
+ .getState()
117
+ .subscribeToUpdates(handleUpdate);
118
+ return unsubscribe;
119
+ }, [stateKey, plugin, options, deconstructed, metadataContext]);
120
+
121
+ useEffect(() => {
122
+ if (!plugin.onFormUpdate) return;
123
+
124
+ const handleFormUpdate = (
125
+ event: ClientActivityEvent // Use the proper type
126
+ ) => {
127
+ if (event.stateKey === stateKey) {
128
+ plugin.onFormUpdate!({
129
+ stateKey,
130
+ pluginName: plugin.name,
131
+ path: event.path,
132
+ event: event, // Pass the whole event through, not a transformed version
133
+ options,
134
+ hookData: hookDataRef.current,
135
+ ...deconstructed,
136
+ ...metadataContext,
137
+ });
138
+ }
139
+ };
140
+
141
+ const unsubscribe = pluginStore
142
+ .getState()
143
+ .subscribeToFormUpdates(handleFormUpdate);
144
+ return unsubscribe;
145
+ }, [stateKey, plugin, options, deconstructed, metadataContext]);
146
+
147
+ return null;
148
+ }
149
+ );
150
+ /**
151
+ * The main orchestrator component. It reads from the central pluginStore
152
+ * and renders a `PluginInstance` controller for each active plugin.
153
+ */
154
+ export function PluginRunner({ children }: { children: React.ReactNode }) {
155
+ // A simple way to force a re-render when the store changes.
156
+ const [, forceUpdate] = useReducer((c) => c + 1, 0);
157
+
158
+ // Subscribe to the store. When plugins or their options are added/removed,
159
+ // this component will re-render to update the list of PluginInstances.
160
+ useEffect(() => {
161
+ const unsubscribe = pluginStore.subscribe(forceUpdate);
162
+ return unsubscribe;
163
+ }, []);
164
+
165
+ const { pluginOptions, stateHandlers, registeredPlugins } =
166
+ pluginStore.getState();
167
+
168
+ return (
169
+ <>
170
+ {/*
171
+ This declarative mapping is the core of the solution.
172
+ React will now manage adding and removing `PluginInstance` components
173
+ as the application state changes, ensuring hooks are handled safely.
174
+ */}
175
+ {Array.from(pluginOptions.entries()).map(([stateKey, pluginMap]) => {
176
+ const stateHandler = stateHandlers.get(stateKey);
177
+ if (!stateHandler) {
178
+ return null; // Don't render a runner if the state handler isn't ready.
179
+ }
180
+
181
+ return Array.from(pluginMap.entries()).map(([pluginName, options]) => {
182
+ const plugin = registeredPlugins.find((p) => p.name === pluginName);
183
+ if (!plugin) {
184
+ return null; // Don't render if the plugin is not in the registered list.
185
+ }
186
+
187
+ // Render a dedicated, memoized controller for this specific plugin configuration.
188
+ return (
189
+ <PluginInstance
190
+ key={`${stateKey}:${pluginName}`}
191
+ stateKey={stateKey}
192
+ plugin={plugin}
193
+ options={options}
194
+ stateHandler={stateHandler}
195
+ />
196
+ );
197
+ });
198
+ })}
199
+
200
+ {children}
201
+ </>
202
+ );
203
+ }
package/src/index.ts CHANGED
@@ -3,3 +3,5 @@ export * from './CogsStateClient.js';
3
3
  export * from './utility.js';
4
4
  export * from './TRPCValidationLink.js';
5
5
  export * from './store.js';
6
+ export * from './plugins.js';
7
+ export * from './PluginRunner.js';
@@ -0,0 +1,176 @@
1
+ import { create } from 'zustand';
2
+ import type { PluginData, StateObject, UpdateTypeDetail } from './CogsState';
3
+ import type { CogsPlugin } from './plugins';
4
+ export type ClientActivityEvent = {
5
+ stateKey: string;
6
+ path: string[];
7
+ timestamp: number;
8
+ duration?: number;
9
+ } & (
10
+ | { activityType: 'focus'; details: { cursorPosition?: number } }
11
+ | { activityType: 'blur'; details: { duration: number } }
12
+ | {
13
+ activityType: 'input';
14
+ details: {
15
+ value: any;
16
+ inputLength?: number;
17
+ isComposing?: boolean;
18
+ isPasting?: boolean;
19
+ keystrokeCount?: number;
20
+ };
21
+ }
22
+ | {
23
+ activityType: 'select';
24
+ details: {
25
+ selectionStart: number;
26
+ selectionEnd: number;
27
+ selectedText?: string;
28
+ };
29
+ }
30
+ | { activityType: 'hover_enter'; details: { cursorPosition?: number } }
31
+ | { activityType: 'hover_exit'; details: { duration: number } }
32
+ | {
33
+ activityType: 'scroll';
34
+ details: { scrollTop: number; scrollLeft: number };
35
+ }
36
+ | { activityType: 'cursor_move'; details: { cursorPosition: number } }
37
+ );
38
+
39
+ type PluginRegistryStore = {
40
+ stateHandlers: Map<string, StateObject<any>>; // stateKey -> handler
41
+ registerStateHandler: (stateKey: string, handler: StateObject<any>) => void;
42
+ registeredPlugins: readonly CogsPlugin<any, any, any, any, any>[];
43
+ setRegisteredPlugins: (
44
+ plugins: readonly CogsPlugin<any, any, any, any, any>[]
45
+ ) => void;
46
+
47
+ // Store options keyed by stateKey and pluginName
48
+ pluginOptions: Map<string, Map<string, any>>; // stateKey -> pluginName -> options
49
+ setPluginOptionsForState: (
50
+ stateKey: string,
51
+ pluginOptions: Record<string, any>
52
+ ) => void;
53
+
54
+ // Get all plugin configs for a specific stateKey
55
+ getPluginConfigsForState: (stateKey: string) => Array<{
56
+ plugin: CogsPlugin<any, any, any, any, any>;
57
+ options: any;
58
+ }>;
59
+ updateSubscribers: Set<(update: UpdateTypeDetail) => void>;
60
+ subscribeToUpdates: (
61
+ callback: (update: UpdateTypeDetail) => void
62
+ ) => () => void;
63
+ notifyUpdate: (update: UpdateTypeDetail) => void;
64
+ formUpdateSubscribers: Set<(event: ClientActivityEvent) => void>;
65
+ subscribeToFormUpdates: (
66
+ callback: (event: ClientActivityEvent) => void
67
+ ) => () => void;
68
+ notifyFormUpdate: (event: ClientActivityEvent) => void;
69
+ hookResults: Map<string, Map<string, any>>; // stateKey -> pluginName -> hook
70
+ setHookResult: (stateKey: string, pluginName: string, data: any) => void;
71
+ getHookResult: (stateKey: string, pluginName: string) => any | undefined;
72
+ removeHookResult: (stateKey: string, pluginName: string) => void;
73
+ };
74
+
75
+ export const pluginStore = create<PluginRegistryStore>((set, get) => ({
76
+ stateHandlers: new Map(),
77
+ registerStateHandler: (stateKey, handler) =>
78
+ set((state) => {
79
+ const newMap = new Map(state.stateHandlers);
80
+ newMap.set(stateKey, handler);
81
+ console.log('addign handler', stateKey, handler);
82
+ return { stateHandlers: newMap };
83
+ }),
84
+ registeredPlugins: [],
85
+ pluginOptions: new Map(),
86
+
87
+ setRegisteredPlugins: (plugins) => set({ registeredPlugins: plugins }),
88
+
89
+ setPluginOptionsForState: (stateKey, pluginOptions) =>
90
+ set((state) => {
91
+ const newMap = new Map(state.pluginOptions);
92
+ const statePluginMap = new Map();
93
+
94
+ // Store each plugin's options
95
+ Object.entries(pluginOptions).forEach(([pluginName, options]) => {
96
+ // Only store if this is actually a registered plugin
97
+ if (state.registeredPlugins.some((p) => p.name === pluginName)) {
98
+ statePluginMap.set(pluginName, options);
99
+ }
100
+ });
101
+
102
+ if (statePluginMap.size > 0) {
103
+ newMap.set(stateKey, statePluginMap);
104
+ }
105
+
106
+ return { pluginOptions: newMap };
107
+ }),
108
+
109
+ getPluginConfigsForState: (stateKey) => {
110
+ const state = get();
111
+ const stateOptions = state.pluginOptions.get(stateKey);
112
+ if (!stateOptions) return [];
113
+
114
+ return state.registeredPlugins
115
+ .map((plugin) => {
116
+ const options = stateOptions.get(plugin.name);
117
+ if (options !== undefined) {
118
+ return { plugin, options };
119
+ }
120
+ return null;
121
+ })
122
+ .filter(Boolean) as Array<{
123
+ plugin: CogsPlugin<any, any, any, any, any>;
124
+ options: any;
125
+ }>;
126
+ },
127
+ updateSubscribers: new Set(),
128
+ subscribeToUpdates: (callback) => {
129
+ const subscribers = get().updateSubscribers;
130
+ subscribers.add(callback);
131
+ // Return an unsubscribe function
132
+ return () => {
133
+ get().updateSubscribers.delete(callback);
134
+ };
135
+ },
136
+ notifyUpdate: (update) => {
137
+ // Call all registered subscribers with the update details
138
+ get().updateSubscribers.forEach((callback) => callback(update));
139
+ },
140
+ formUpdateSubscribers: new Set(),
141
+ subscribeToFormUpdates: (callback) => {
142
+ const subscribers = get().formUpdateSubscribers;
143
+ subscribers.add(callback);
144
+ return () => {
145
+ get().formUpdateSubscribers.delete(callback);
146
+ };
147
+ },
148
+ notifyFormUpdate: (event) => {
149
+ get().formUpdateSubscribers.forEach((callback) => callback(event));
150
+ },
151
+ hookResults: new Map(),
152
+
153
+ setHookResult: (stateKey, pluginName, data) =>
154
+ set((state) => {
155
+ const next = new Map(state.hookResults);
156
+ const byPlugin = new Map(next.get(stateKey) ?? new Map());
157
+ if (data === undefined) byPlugin.delete(pluginName);
158
+ else byPlugin.set(pluginName, data);
159
+ if (byPlugin.size > 0) next.set(stateKey, byPlugin);
160
+ else next.delete(stateKey);
161
+ return { hookResults: next };
162
+ }),
163
+
164
+ getHookResult: (stateKey, pluginName) =>
165
+ get().hookResults.get(stateKey)?.get(pluginName),
166
+
167
+ removeHookResult: (stateKey, pluginName) =>
168
+ set((state) => {
169
+ const next = new Map(state.hookResults);
170
+ const byPlugin = new Map(next.get(stateKey) ?? new Map());
171
+ byPlugin.delete(pluginName);
172
+ if (byPlugin.size > 0) next.set(stateKey, byPlugin);
173
+ else next.delete(stateKey);
174
+ return { hookResults: next };
175
+ }),
176
+ }));