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.
- package/README.md +48 -18
- package/dist/CogsState.d.ts +98 -82
- package/dist/CogsState.d.ts.map +1 -1
- package/dist/CogsState.jsx +1030 -960
- package/dist/CogsState.jsx.map +1 -1
- package/dist/Components.d.ts.map +1 -1
- package/dist/Components.jsx +299 -219
- package/dist/Components.jsx.map +1 -1
- package/dist/PluginRunner.d.ts +10 -0
- package/dist/PluginRunner.d.ts.map +1 -0
- package/dist/PluginRunner.jsx +122 -0
- package/dist/PluginRunner.jsx.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -26
- package/dist/index.js.map +1 -1
- package/dist/pluginStore.d.ts +81 -0
- package/dist/pluginStore.d.ts.map +1 -0
- package/dist/pluginStore.js +52 -0
- package/dist/pluginStore.js.map +1 -0
- package/dist/plugins.d.ts +1323 -0
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +76 -0
- package/dist/plugins.js.map +1 -0
- package/dist/store.d.ts +50 -15
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +509 -470
- package/dist/store.js.map +1 -1
- package/dist/utility.d.ts +1 -1
- package/dist/utility.d.ts.map +1 -1
- package/dist/utility.js +12 -12
- package/dist/utility.js.map +1 -1
- package/dist/validation.d.ts +7 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +39 -0
- package/dist/validation.js.map +1 -0
- package/package.json +13 -3
- package/src/CogsState.tsx +657 -457
- package/src/Components.tsx +291 -194
- package/src/PluginRunner.tsx +203 -0
- package/src/index.ts +2 -0
- package/src/pluginStore.ts +176 -0
- package/src/plugins.ts +544 -0
- package/src/store.ts +748 -493
- package/src/utility.ts +31 -31
- 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
|
@@ -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
|
+
}));
|