cogsbox-state 0.5.482 → 0.5.484

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/src/CogsState.tsx CHANGED
@@ -721,33 +721,19 @@ function setOptions<StateKey, Opt>({
721
721
  if (needToAdd) {
722
722
  setInitialStateOptions(stateKey as string, mergedOptions);
723
723
 
724
- // NEW: Update type info if validation schema was added
725
- const hadSchema =
726
- initialOptions?.validation?.zodSchemaV4 ||
727
- initialOptions?.validation?.zodSchemaV3;
728
- const hasNewSchemaV4 =
729
- mergedOptions.validation?.zodSchemaV4 &&
730
- !initialOptions?.validation?.zodSchemaV4;
731
- const hasNewSchemaV3 =
732
- mergedOptions.validation?.zodSchemaV3 &&
733
- !initialOptions?.validation?.zodSchemaV3;
734
-
735
- if (!hadSchema && (hasNewSchemaV4 || hasNewSchemaV3)) {
736
- if (hasNewSchemaV4) {
737
- updateShadowTypeInfo(
738
- stateKey as string,
739
- mergedOptions.validation.zodSchemaV4,
740
- 'zod4'
741
- );
742
- } else if (hasNewSchemaV3) {
743
- updateShadowTypeInfo(
744
- stateKey as string,
745
- mergedOptions.validation.zodSchemaV3,
746
- 'zod3'
747
- );
724
+ const schemaV4 = mergedOptions.validation?.zodSchemaV4;
725
+ const schemaV3 = mergedOptions.validation?.zodSchemaV3;
726
+ const schemaChanged =
727
+ schemaV4 !== initialOptions?.validation?.zodSchemaV4 ||
728
+ schemaV3 !== initialOptions?.validation?.zodSchemaV3;
729
+
730
+ if (schemaChanged && (schemaV4 || schemaV3)) {
731
+ if (schemaV4) {
732
+ updateShadowTypeInfo(stateKey as string, schemaV4, 'zod4');
733
+ } else if (schemaV3) {
734
+ updateShadowTypeInfo(stateKey as string, schemaV3, 'zod3');
748
735
  }
749
736
 
750
- // Notify components to re-render with updated validation
751
737
  notifyComponents(stateKey as string);
752
738
  }
753
739
  }
@@ -828,31 +814,42 @@ type KeyedKeys<P> = {
828
814
  : never;
829
815
  }[keyof P];
830
816
 
817
+ type PluginOptionEntry<
818
+ P,
819
+ StateKey extends PropertyKey,
820
+ > = P extends undefined
821
+ ? never
822
+ : P extends Record<string, any>
823
+ ? Prettify<
824
+ Partial<Pick<P, Exclude<keyof P, KeyedKeys<P>>>> & {
825
+ [K in KeyedKeys<P> as StateKey extends keyof NonNullable<
826
+ P[K]
827
+ >['map']
828
+ ? NonNullable<P[K]>['map'][StateKey] extends undefined
829
+ ? never
830
+ : keyof NonNullable<P[K]>['map'][StateKey] extends never
831
+ ? never
832
+ : K
833
+ : never]: CleanIntersection<
834
+ StateKey extends keyof NonNullable<P[K]>['map']
835
+ ? NonNullable<P[K]>['map'][StateKey]
836
+ : never
837
+ >;
838
+ }
839
+ >
840
+ : P extends object
841
+ ? Partial<P> extends P
842
+ ? Partial<P>
843
+ : P
844
+ : P;
845
+
831
846
  type PluginOptionsForState<
832
847
  PluginOptions,
833
848
  StateKey extends PropertyKey,
834
849
  > = {
835
- [PName in keyof PluginOptions]?: PluginOptions[PName] extends infer P
836
- ? P extends Record<string, any>
837
- ? Prettify<
838
- Partial<Pick<P, Exclude<keyof P, KeyedKeys<P>>>> & {
839
- [K in KeyedKeys<P> as StateKey extends keyof NonNullable<
840
- P[K]
841
- >['map']
842
- ? NonNullable<P[K]>['map'][StateKey] extends undefined
843
- ? never
844
- : keyof NonNullable<P[K]>['map'][StateKey] extends never
845
- ? never
846
- : K
847
- : never]: CleanIntersection<
848
- StateKey extends keyof NonNullable<P[K]>['map']
849
- ? NonNullable<P[K]>['map'][StateKey]
850
- : never
851
- >;
852
- }
853
- >
854
- : P
855
- : never;
850
+ [PName in keyof PluginOptions as PluginOptions[PName] extends undefined
851
+ ? never
852
+ : PName]?: PluginOptionEntry<PluginOptions[PName], StateKey>;
856
853
  };
857
854
 
858
855
  type UseCogsStateOptions<
@@ -987,29 +984,8 @@ export const createCogsState = <
987
984
  const useCogsState = <StateKey extends StateKeys>(
988
985
  stateKey: StateKey,
989
986
  options?: Prettify<
990
- OptionsType<StateSlice<StateKey>, never> & {
991
- [PName in keyof PluginOptions]?: PluginOptions[PName] extends infer P
992
- ? P extends Record<string, any>
993
- ? Prettify<
994
- Partial<Pick<P, Exclude<keyof P, KeyedKeys<P>>>> & {
995
- [K in KeyedKeys<P> as StateKey extends keyof NonNullable<
996
- P[K]
997
- >['map']
998
- ? NonNullable<P[K]>['map'][StateKey] extends undefined
999
- ? never
1000
- : keyof NonNullable<P[K]>['map'][StateKey] extends never
1001
- ? never
1002
- : K
1003
- : never]: CleanIntersection<
1004
- StateKey extends keyof NonNullable<P[K]>['map']
1005
- ? NonNullable<P[K]>['map'][StateKey]
1006
- : never
1007
- >;
1008
- }
1009
- >
1010
- : P
1011
- : never;
1012
- }
987
+ OptionsType<StateSlice<StateKey>, never> &
988
+ PluginOptionsForState<PluginOptions, StateKey>
1013
989
  >
1014
990
  ): StateObject<StateSlice<StateKey>> => {
1015
991
  const [componentId] = useState(options?.componentId ?? uuidv4());
@@ -1040,23 +1016,33 @@ export const createCogsState = <
1040
1016
  serverState: options?.serverState,
1041
1017
  });
1042
1018
 
1043
- useEffect(() => {
1019
+ useLayoutEffect(() => {
1044
1020
  if (options) {
1045
1021
  pluginStore
1046
1022
  .getState()
1047
1023
  .setPluginOptionsForState(stateKey as string, options);
1048
1024
  }
1049
1025
  }, [stateKey, options]);
1050
- useEffect(() => {
1026
+
1027
+ useLayoutEffect(() => {
1051
1028
  pluginStore
1052
1029
  .getState()
1053
- .stateHandlers.set(stateKey as string, updater as any);
1030
+ .registerStateHandler(stateKey as string, updater as any);
1054
1031
 
1055
1032
  return () => {
1056
- pluginStore.getState().stateHandlers.delete(stateKey as string);
1033
+ pluginStore.getState().unregisterStateHandler(stateKey as string);
1057
1034
  };
1058
1035
  }, [stateKey, updater]);
1059
1036
 
1037
+ useLayoutEffect(() => {
1038
+ const validation = getInitialOptions(stateKey as string)?.validation;
1039
+ if (validation?.zodSchemaV4) {
1040
+ updateShadowTypeInfo(stateKey as string, validation.zodSchemaV4, 'zod4');
1041
+ } else if (validation?.zodSchemaV3) {
1042
+ updateShadowTypeInfo(stateKey as string, validation.zodSchemaV3, 'zod3');
1043
+ }
1044
+ });
1045
+
1060
1046
  return updater as StateObject<StateSlice<StateKey>>;
1061
1047
  };
1062
1048
 
@@ -1737,7 +1723,7 @@ export function useCogsStateFn<
1737
1723
  defaultState?: TStateObject;
1738
1724
  syncOptions?: SyncOptionsType<any>;
1739
1725
  } & OptionsType<TStateObject> = {}
1740
- ) {
1726
+ ): StateObject<TStateObject, TPlugins> {
1741
1727
  const [reactiveForce, forceUpdate] = useState({}); //this is the key to reactivity
1742
1728
  const { sessionId } = useCogsConfig();
1743
1729
  let noStateKey = stateKey ? false : true;
@@ -1937,6 +1923,14 @@ export function useCogsStateFn<
1937
1923
 
1938
1924
  const { value: resolvedState, source, timestamp } = resolveInitialState();
1939
1925
  initializeShadowState(thisKey, resolvedState);
1926
+
1927
+ const validation = getInitialOptions(thisKey as string)?.validation;
1928
+ if (validation?.zodSchemaV4) {
1929
+ updateShadowTypeInfo(thisKey as string, validation.zodSchemaV4, 'zod4');
1930
+ } else if (validation?.zodSchemaV3) {
1931
+ updateShadowTypeInfo(thisKey as string, validation.zodSchemaV3, 'zod3');
1932
+ }
1933
+
1940
1934
  setShadowMetadata(thisKey, [], {
1941
1935
  stateSource: source,
1942
1936
  lastServerSync: source === 'server' ? timestamp : undefined,
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import React, { createContext, useContext } from "react";
3
+ import React, { createContext, useContext, type ReactNode } from "react";
4
4
  type ConfigType = { sessionId?: string };
5
5
  export const config: ConfigType = {
6
6
  sessionId: undefined,
@@ -14,7 +14,7 @@ export function CogsStateClient({
14
14
  children,
15
15
  sessionId,
16
16
  }: {
17
- children: React.ReactNode;
17
+ children: ReactNode;
18
18
  sessionId?: string;
19
19
  }) {
20
20
  return (
@@ -20,6 +20,7 @@ import React, {
20
20
  useRef,
21
21
  useState,
22
22
  useMemo,
23
+ type ReactNode,
23
24
  } from 'react';
24
25
  import { getGlobalStore, ValidationError, ValidationSeverity } from './store';
25
26
  import { useInView } from 'react-intersection-observer';
@@ -46,7 +47,7 @@ export type ValidationWrapperProps = {
46
47
  formOpts?: FormOptsType;
47
48
  path: string[];
48
49
  stateKey: string;
49
- children: React.ReactNode;
50
+ children: ReactNode;
50
51
  };
51
52
 
52
53
  export function ValidationWrapper({
@@ -241,7 +242,7 @@ export function FormElementWrapper({
241
242
  componentId: string;
242
243
  meta?: any;
243
244
  }) => any;
244
- renderFn: (params: FormElementParams<any>) => React.ReactNode;
245
+ renderFn: (params: FormElementParams<any>) => ReactNode;
245
246
  formOpts?: FormOptsType;
246
247
  setState: any;
247
248
  }) {
@@ -335,21 +336,28 @@ export function FormElementWrapper({
335
336
 
336
337
  const debouncedUpdate = useCallback(
337
338
  (newValue: any) => {
339
+ const liveTypeInfo =
340
+ getGlobalStore.getState().getShadowNode(stateKey, path)?._meta
341
+ ?.typeInfo ?? typeInfo;
342
+
338
343
  // Type conversion logic (keep existing)
339
- if (typeInfo) {
340
- if (typeInfo.type === 'number' && typeof newValue === 'string') {
344
+ if (liveTypeInfo) {
345
+ if (liveTypeInfo.type === 'number' && typeof newValue === 'string') {
341
346
  newValue =
342
347
  newValue === ''
343
- ? typeInfo.nullable
348
+ ? liveTypeInfo.nullable
344
349
  ? null
345
- : (typeInfo.default ?? 0)
350
+ : (liveTypeInfo.default ?? 0)
346
351
  : Number(newValue);
347
352
  } else if (
348
- typeInfo.type === 'boolean' &&
353
+ liveTypeInfo.type === 'boolean' &&
349
354
  typeof newValue === 'string'
350
355
  ) {
351
356
  newValue = newValue === 'true' || newValue === '1';
352
- } else if (typeInfo.type === 'date' && typeof newValue === 'string') {
357
+ } else if (
358
+ liveTypeInfo.type === 'date' &&
359
+ typeof newValue === 'string'
360
+ ) {
353
361
  newValue = new Date(newValue);
354
362
  }
355
363
  } else {
@@ -626,7 +634,7 @@ export function IsolatedComponentWrapper({
626
634
  }
627
635
  // 1. Define the MINIMAL props needed.
628
636
  type PluginWrapperProps = {
629
- children: React.ReactNode;
637
+ children: ReactNode;
630
638
  stateKey: string;
631
639
  path: string[];
632
640
  pluginName: string;
@@ -1,4 +1,12 @@
1
- import React, { useEffect, useMemo, useState, useRef, useReducer } from 'react';
1
+ import React, {
2
+ useEffect,
3
+ useLayoutEffect,
4
+ useMemo,
5
+ useState,
6
+ useRef,
7
+ useSyncExternalStore,
8
+ type ReactNode,
9
+ } from 'react';
2
10
  import { ClientActivityEvent, pluginStore } from './pluginStore';
3
11
  import { isDeepEqual } from './utility';
4
12
  import {
@@ -68,7 +76,7 @@ const PluginInstance = React.memo(
68
76
  const lastProcessedOptionsRef = useRef<any>();
69
77
  const [isInitialTransform, setIsInitialTransform] = useState(true);
70
78
 
71
- useEffect(() => {
79
+ useLayoutEffect(() => {
72
80
  if (plugin.transformState) {
73
81
  if (!isDeepEqual(options, lastProcessedOptionsRef.current)) {
74
82
  plugin.transformState({
@@ -168,52 +176,42 @@ const PluginInstance = React.memo(
168
176
  * The main orchestrator component. It reads from the central pluginStore
169
177
  * and renders a `PluginInstance` controller for each active plugin.
170
178
  */
171
- export function PluginRunner({ children }: { children: React.ReactNode }) {
172
- // A simple way to force a re-render when the store changes.
173
- const [, forceUpdate] = useReducer((c) => c + 1, 0);
174
-
175
- // Subscribe to the store. When plugins or their options are added/removed,
176
- // this component will re-render to update the list of PluginInstances.
177
- useEffect(() => {
178
- const unsubscribe = pluginStore.subscribe(forceUpdate);
179
-
180
- return unsubscribe;
181
- }, []);
182
-
179
+ export function PluginRunner({ children }: { children: ReactNode }) {
183
180
  const { pluginOptions, stateHandlers, registeredPlugins } =
184
- pluginStore.getState();
181
+ useSyncExternalStore(
182
+ pluginStore.subscribe,
183
+ () => pluginStore.getState(),
184
+ () => pluginStore.getState()
185
+ );
186
+
187
+ const pluginNeedsRunner = (
188
+ plugin: CogsPlugin<any, any, any, any, any, any, any>
189
+ ) =>
190
+ !!(
191
+ plugin.useHook ||
192
+ plugin.transformState ||
193
+ plugin.onUpdate ||
194
+ plugin.onFormUpdate
195
+ );
185
196
 
186
197
  return (
187
198
  <>
188
- {/*
189
- This declarative mapping is the core of the solution.
190
- React will now manage adding and removing `PluginInstance` components
191
- as the application state changes, ensuring hooks are handled safely.
192
- */}
193
- {Array.from(pluginOptions.entries()).map(([stateKey, pluginMap]) => {
194
- const stateHandler = stateHandlers.get(stateKey);
195
- if (!stateHandler) {
196
- return null; // Don't render a runner if the state handler isn't ready.
197
- }
198
-
199
- return Array.from(pluginMap.entries()).map(([pluginName, options]) => {
200
- const plugin = registeredPlugins.find((p) => p.name === pluginName);
201
- if (!plugin) {
202
- return null; // Don't render if the plugin is not in the registered list.
203
- }
199
+ {Array.from(stateHandlers.entries()).flatMap(([stateKey, stateHandler]) =>
200
+ registeredPlugins.filter(pluginNeedsRunner).map((plugin) => {
201
+ const options =
202
+ pluginOptions.get(stateKey)?.get(plugin.name) ?? {};
204
203
 
205
- // Render a dedicated, memoized controller for this specific plugin configuration.
206
204
  return (
207
205
  <PluginInstance
208
- key={`${stateKey}:${pluginName}`}
206
+ key={`${stateKey}:${plugin.name}`}
209
207
  stateKey={stateKey}
210
208
  plugin={plugin}
211
209
  options={options}
212
210
  stateHandler={stateHandler}
213
211
  />
214
212
  );
215
- });
216
- })}
213
+ })
214
+ )}
217
215
 
218
216
  {children}
219
217
  </>
@@ -39,6 +39,7 @@ export type ClientActivityEvent = {
39
39
  type PluginRegistryStore = {
40
40
  stateHandlers: Map<string, StateObject<any>>; // stateKey -> handler
41
41
  registerStateHandler: (stateKey: string, handler: StateObject<any>) => void;
42
+ unregisterStateHandler: (stateKey: string) => void;
42
43
  registeredPlugins: readonly CogsPlugin<any, any, any, any, any>[];
43
44
  setRegisteredPlugins: (
44
45
  plugins: readonly CogsPlugin<any, any, any, any, any>[]
@@ -78,7 +79,12 @@ export const pluginStore = create<PluginRegistryStore>((set, get) => ({
78
79
  set((state) => {
79
80
  const newMap = new Map(state.stateHandlers);
80
81
  newMap.set(stateKey, handler);
81
- console.log('addign handler', stateKey, handler);
82
+ return { stateHandlers: newMap };
83
+ }),
84
+ unregisterStateHandler: (stateKey) =>
85
+ set((state) => {
86
+ const newMap = new Map(state.stateHandlers);
87
+ newMap.delete(stateKey);
82
88
  return { stateHandlers: newMap };
83
89
  }),
84
90
  registeredPlugins: [],
package/src/plugins.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import type React from 'react';
2
+ import type { ReactNode, RefObject } from 'react';
3
3
  import { StateObject, UpdateTypeDetail } from './CogsState';
4
4
  import {
5
5
  getGlobalStore,
@@ -7,7 +7,6 @@ import {
7
7
  setAllFieldsDisabled,
8
8
  } from './store';
9
9
  import { ClientActivityEvent } from './pluginStore';
10
- import { RefObject } from 'react';
11
10
 
12
11
  type Prettify<T> = { [K in keyof T]: T[K] } & {};
13
12
 
@@ -228,7 +227,7 @@ export type FormWrapperParams<
228
227
  TFieldMetaData,
229
228
  TStateSlice = any,
230
229
  > = ScopedMetadataMethods<TFieldMetaData> & {
231
- element: React.ReactNode;
230
+ element: ReactNode;
232
231
  path: string[];
233
232
  stateKey: string;
234
233
  options: TOptions;
@@ -307,7 +306,7 @@ export type CogsPlugin<
307
306
  TFieldMetaData,
308
307
  any
309
308
  >
310
- ) => React.ReactNode;
309
+ ) => ReactNode;
311
310
 
312
311
  chainMethods?: TChainMethods;
313
312
  };
@@ -602,7 +601,7 @@ export type PluginFormWrapperFn<
602
601
  TFieldMetaData,
603
602
  any
604
603
  >
605
- ) => React.ReactNode;
604
+ ) => ReactNode;
606
605
 
607
606
  export type CogsPluginBuilder<
608
607
  TName extends string,
@@ -774,7 +773,7 @@ export type CogsPluginBuilder<
774
773
  true,
775
774
  TNewState
776
775
  >;
777
- });
776
+ });
778
777
 
779
778
  export type CreatePluginStart<
780
779
  TName extends string,
package/src/store.ts CHANGED
@@ -316,16 +316,25 @@ function getTypeFromZodSchema(
316
316
 
317
317
  const typeIdentifier = def.typeName || def.type || current._type;
318
318
 
319
- // --- START: THE CRITICAL FIX FOR ZodUnion ---
320
319
  if (typeIdentifier === 'ZodUnion' || typeIdentifier === 'union') {
321
320
  if (def.options && def.options.length > 0) {
322
- current = def.options[0]; // Proceed by analyzing the FIRST option of the union
323
- continue; // Restart the loop with the new schema
321
+ const preferredOption = def.options.find((option: any) => {
322
+ const optionDef = option?.def || option?._def;
323
+ const optionType =
324
+ optionDef?.typeName || optionDef?.type || option?._type;
325
+ return (
326
+ optionType !== 'ZodNull' &&
327
+ optionType !== 'null' &&
328
+ optionType !== 'ZodUndefined' &&
329
+ optionType !== 'undefined'
330
+ );
331
+ });
332
+ current = preferredOption ?? def.options[0];
333
+ continue;
324
334
  } else {
325
- break; // Union with no options, cannot determine type
335
+ break;
326
336
  }
327
337
  }
328
- // --- END: THE CRITICAL FIX ---
329
338
 
330
339
  if (typeIdentifier === 'ZodOptional' || typeIdentifier === 'optional') {
331
340
  isOptional = true;
@@ -684,9 +693,10 @@ function getSchemaAtPath(schema: any, path: string[]): any {
684
693
  const typeIdentifier = def?.typeName || def?.type || containerSchema._type;
685
694
 
686
695
  if (typeIdentifier === 'ZodObject' || typeIdentifier === 'object') {
687
- // VITAL FIX: Check for `shape` inside `def` first, then on the schema itself.
696
+ const rawShape =
697
+ def?.shape ?? containerSchema.shape ?? containerSchema._shape;
688
698
  const shape =
689
- def?.shape || containerSchema.shape || containerSchema._shape;
699
+ typeof rawShape === 'function' ? rawShape() : rawShape;
690
700
  currentSchema = shape?.[segment];
691
701
  } else if (typeIdentifier === 'ZodArray' || typeIdentifier === 'array') {
692
702
  // For arrays, the next schema is always the element's schema.
@@ -1006,6 +1016,13 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
1006
1016
 
1007
1017
  const storageKey = Array.isArray(initialState) ? `[${key}` : key;
1008
1018
  shadowStateStore.set(storageKey, newRoot);
1019
+
1020
+ const latestOptions = get().getInitialOptions(key);
1021
+ if (latestOptions?.validation?.zodSchemaV4) {
1022
+ updateShadowTypeInfo(key, latestOptions.validation.zodSchemaV4, 'zod4');
1023
+ } else if (latestOptions?.validation?.zodSchemaV3) {
1024
+ updateShadowTypeInfo(key, latestOptions.validation.zodSchemaV3, 'zod3');
1025
+ }
1009
1026
  },
1010
1027
  getShadowNode: (key: string, path: string[]): ShadowNode | undefined => {
1011
1028
  let current: any =