cogsbox-state 0.5.488 → 0.5.490

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
@@ -58,31 +58,28 @@ import { ZodType } from 'zod/v4';
58
58
 
59
59
  export type Prettify<T> = T extends any ? { [K in keyof T]: T[K] } : never;
60
60
 
61
- export type SyncInfo = {
62
- timeStamp: number;
63
- userId: number;
64
- };
65
-
66
- export type ValidationFieldSummary = {
67
- status: ValidationStatus;
68
- severity: ValidationSeverity;
69
- hasErrors: boolean;
70
- hasWarnings: boolean;
71
- message: string;
72
- errors: string[];
73
- warnings: string[];
74
- allErrors: Array<ValidationError & { path: string[] }>;
75
- path: string[];
76
- getData: () => unknown;
77
- };
78
-
79
- type ValidationSummaryArray = ValidationFieldSummary[];
80
-
81
- export type FormElementParams<T> = StateObject<T> & {
82
- $inputProps: {
83
- ref?: RefObject<any>;
84
- value?: T extends boolean ? never : T;
85
- onChange?: (
61
+ // Removed: SyncInfo is now owned by Shape Plugin
62
+
63
+ export type ValidationFieldSummary = {
64
+ status: ValidationStatus;
65
+ severity: ValidationSeverity;
66
+ hasErrors: boolean;
67
+ hasWarnings: boolean;
68
+ message: string;
69
+ errors: string[];
70
+ warnings: string[];
71
+ allErrors: Array<ValidationError & { path: string[] }>;
72
+ path: string[];
73
+ getData: () => unknown;
74
+ };
75
+
76
+ type ValidationSummaryArray = ValidationFieldSummary[];
77
+
78
+ export type FormElementParams<T> = StateObject<T> & {
79
+ $inputProps: {
80
+ ref?: RefObject<any>;
81
+ value?: T extends boolean ? never : T;
82
+ onChange?: (
86
83
  event: ChangeEvent<
87
84
  HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
88
85
  >
@@ -90,17 +87,17 @@ export type FormElementParams<T> = StateObject<T> & {
90
87
  onBlur?: (
91
88
  event: FocusEvent<
92
89
  HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
93
- >
94
- ) => void;
95
- };
96
- status: ValidationStatus;
97
- severity: ValidationSeverity;
98
- hasErrors: boolean;
99
- hasWarnings: boolean;
100
- allErrors: ValidationError[];
101
- message: string;
102
- getData: () => T;
103
- };
90
+ >
91
+ ) => void;
92
+ };
93
+ status: ValidationStatus;
94
+ severity: ValidationSeverity;
95
+ hasErrors: boolean;
96
+ hasWarnings: boolean;
97
+ allErrors: ValidationError[];
98
+ message: string;
99
+ getData: () => T;
100
+ };
104
101
 
105
102
  export type StateKeys = string;
106
103
 
@@ -206,16 +203,15 @@ export type EndType<
206
203
  $get: () => T;
207
204
  $$get: () => T;
208
205
  $$derive: <R>(fn: EffectFunction<T, R>) => R;
209
- $_status: 'fresh' | 'dirty' | 'synced' | 'restored' | 'unknown';
210
- $getStatus: () => 'fresh' | 'dirty' | 'synced' | 'restored' | 'unknown';
211
- $showValidationErrors: () => string[];
212
- $validationErrors: {
213
- (): ValidationSummaryArray;
214
- <K extends Extract<keyof NonNullable<T>, string>>(
215
- keys: readonly K[]
216
- ): ValidationSummaryArray;
217
- };
218
- $setValidation: (ctx: string) => void;
206
+ // Removed: $_status and $getStatus are now owned by Shape Plugin
207
+ $showValidationErrors: () => string[];
208
+ $validationErrors: {
209
+ (): ValidationSummaryArray;
210
+ <K extends Extract<keyof NonNullable<T>, string>>(
211
+ keys: readonly K[]
212
+ ): ValidationSummaryArray;
213
+ };
214
+ $setValidation: (ctx: string) => void;
219
215
  $removeValidation: (ctx: string) => void;
220
216
  $isSelected: boolean;
221
217
  $setSelected: (value: boolean) => void;
@@ -230,7 +226,7 @@ export type EndType<
230
226
  };
231
227
  $removeStorage: () => void;
232
228
  $setRaw: (value: T) => void;
233
- $sync: () => void;
229
+ // Removed: $sync is now owned by Shape Plugin
234
230
  $validationWrapper: ({
235
231
  children,
236
232
  hideMessage,
@@ -238,7 +234,7 @@ export type EndType<
238
234
  children: ReactNode;
239
235
  hideMessage?: boolean;
240
236
  }) => JSX.Element;
241
- $lastSynced?: SyncInfo;
237
+ // Removed: $lastSynced is now owned by Shape Plugin
242
238
  };
243
239
 
244
240
  // Helper type for element returned from array methods
@@ -383,21 +379,21 @@ export type StateObject<
383
379
  : {}) & // Fallback to {} since we intersect EndType below anyway
384
380
  EndType<T, TPlugins> & {
385
381
  $toggle: NonNullable<T> extends boolean ? () => void : never;
386
- $validate: {
387
- (): { success: boolean; data?: T; error?: any };
388
- <K extends Extract<keyof NonNullable<T>, string>>(
389
- keys: readonly K[]
390
- ): {
391
- success: boolean;
392
- results: Array<{
393
- key: K;
394
- path: string[];
395
- success: boolean;
396
- data?: unknown;
397
- error?: any;
398
- }>;
399
- };
400
- };
382
+ $validate: {
383
+ (): { success: boolean; data?: T; error?: any };
384
+ <K extends Extract<keyof NonNullable<T>, string>>(
385
+ keys: readonly K[]
386
+ ): {
387
+ success: boolean;
388
+ results: Array<{
389
+ key: K;
390
+ path: string[];
391
+ success: boolean;
392
+ data?: unknown;
393
+ error?: any;
394
+ }>;
395
+ };
396
+ };
401
397
  $_componentId: string | null;
402
398
  $getComponents: () => ComponentsType;
403
399
  $_initialState: T;
@@ -405,8 +401,6 @@ export type StateObject<
405
401
  fetchId: (field: keyof NonNullable<T>) => string | number;
406
402
  };
407
403
  $initializeAndMergeShadowState: (newState: any | null) => void;
408
- $_isLoading: boolean;
409
- $_serverState: T;
410
404
  $revertToInitialState: (obj?: { validationKey?: string }) => T;
411
405
  $middleware: (
412
406
  middles: ({
@@ -526,34 +520,6 @@ export type OptionsType<
526
520
  > = CreateStateOptionsType & {
527
521
  log?: boolean;
528
522
  componentId?: string;
529
- syncOptions?: SyncOptionsType<TApiParams>;
530
-
531
- serverState?: {
532
- id?: string | number;
533
- data?: T;
534
- status?: 'pending' | 'error' | 'success' | 'loading';
535
- timestamp?: number;
536
- merge?:
537
- | boolean
538
- | {
539
- strategy: 'append' | 'prepend' | 'diff';
540
- key?: string;
541
- };
542
- };
543
-
544
- sync?: {
545
- action: (state: T) => Promise<{
546
- success: boolean;
547
- data?: any;
548
- error?: any;
549
- errors?: Array<{
550
- path: (string | number)[];
551
- message: string;
552
- }>;
553
- }>;
554
- onSuccess?: (data: any) => void;
555
- onError?: (error: any) => void;
556
- };
557
523
  middleware?: ({ update }: { update: UpdateTypeDetail }) => void;
558
524
 
559
525
  localStorage?: {
@@ -563,7 +529,6 @@ export type OptionsType<
563
529
 
564
530
  reactiveDeps?: (state: T) => any[] | true;
565
531
  reactiveType?: ReactivityType;
566
- syncUpdate?: Partial<UpdateTypeDetail>;
567
532
 
568
533
  defaultState?: T;
569
534
 
@@ -632,13 +597,10 @@ const {
632
597
  insertManyShadowArrayElements,
633
598
  removeShadowArrayElement,
634
599
  setInitialStateOptions,
635
- setServerStateUpdate,
636
- markAsDirty,
637
600
  addPathComponent,
638
601
  clearSelectedIndexesForState,
639
602
  addStateLog,
640
603
  clearSelectedIndex,
641
- getSyncInfo,
642
604
  notifyPathSubscribers,
643
605
  getPluginMetaDataMap,
644
606
  setPluginMetaData,
@@ -914,7 +876,7 @@ type CreateCogsStateReturn<
914
876
  PluginOptionsMap<TPlugins>,
915
877
  StateKey
916
878
  >
917
- ) => StateObject<CogsFullState<State, TPlugins>[StateKey]>;
879
+ ) => StateObject<CogsFullState<State, TPlugins>[StateKey], TPlugins>;
918
880
  setCogsOptionsByKey: <StateKey extends keyof CogsFullState<State, TPlugins>>(
919
881
  stateKey: StateKey,
920
882
  options: CreateStateOptionsType<
@@ -1030,7 +992,7 @@ export const createCogsState = <
1030
992
  OptionsType<StateSlice<StateKey>, never> &
1031
993
  PluginOptionsForState<PluginOptions, StateKey>
1032
994
  >
1033
- ): StateObject<StateSlice<StateKey>> => {
995
+ ): StateObject<StateSlice<StateKey>, TPlugins> => {
1034
996
  const [componentId] = useState(options?.componentId ?? uuidv4());
1035
997
 
1036
998
  const currentOptions = setOptions({
@@ -1048,7 +1010,6 @@ export const createCogsState = <
1048
1010
 
1049
1011
  const updater = useCogsStateFn<StateSlice<StateKey>>(thiState, {
1050
1012
  stateKey: stateKey as string,
1051
- syncUpdate: options?.syncUpdate,
1052
1013
  componentId,
1053
1014
  localStorage: options?.localStorage,
1054
1015
  middleware: options?.middleware,
@@ -1056,7 +1017,6 @@ export const createCogsState = <
1056
1017
  reactiveDeps: options?.reactiveDeps,
1057
1018
  defaultState: options?.defaultState as any,
1058
1019
  dependencies: options?.dependencies,
1059
- serverState: options?.serverState,
1060
1020
  });
1061
1021
 
1062
1022
  useLayoutEffect(() => {
@@ -1086,7 +1046,7 @@ export const createCogsState = <
1086
1046
  }
1087
1047
  });
1088
1048
 
1089
- return updater as StateObject<StateSlice<StateKey>>;
1049
+ return updater as StateObject<StateSlice<StateKey>, TPlugins>;
1090
1050
  };
1091
1051
 
1092
1052
  function setCogsOptionsByKey<StateKey extends StateKeys>(
@@ -1183,8 +1143,6 @@ const saveToLocalStorage = <T,>(
1183
1143
  state,
1184
1144
  lastUpdated: Date.now(),
1185
1145
  lastSyncedWithServer: lastSyncedWithServer ?? existingLastSynced,
1186
- stateSource: shadowMeta?.stateSource,
1187
- baseServerState: shadowMeta?.baseServerState,
1188
1146
  };
1189
1147
 
1190
1148
  // Use SuperJSON serialize to get the json part only
@@ -1251,8 +1209,6 @@ type LocalStorageData<T> = {
1251
1209
  state: T;
1252
1210
  lastUpdated: number;
1253
1211
  lastSyncedWithServer?: number;
1254
- baseServerState?: T; // Keep reference to what server state this is based on
1255
- stateSource?: 'default' | 'server' | 'localStorage'; // Track origin
1256
1212
  };
1257
1213
 
1258
1214
  const notifyComponents = (thisKey: string) => {
@@ -1278,49 +1234,6 @@ const notifyComponents = (thisKey: string) => {
1278
1234
  });
1279
1235
  };
1280
1236
 
1281
- function markEntireStateAsServerSynced(
1282
- stateKey: string,
1283
- path: string[],
1284
- data: any,
1285
- timestamp: number
1286
- ) {
1287
- // Mark current path as synced
1288
- const currentMeta = getShadowMetadata(stateKey, path);
1289
- setShadowMetadata(stateKey, path, {
1290
- ...currentMeta,
1291
- isDirty: false,
1292
- stateSource: 'server',
1293
- lastServerSync: timestamp || Date.now(),
1294
- });
1295
-
1296
- // If it's an array, mark each item as synced
1297
- if (Array.isArray(data)) {
1298
- const arrayMeta = getShadowMetadata(stateKey, path);
1299
- if (arrayMeta?.arrayKeys) {
1300
- arrayMeta.arrayKeys.forEach((itemKey, index) => {
1301
- // Fix: Don't split the itemKey, just use it directly
1302
- const itemPath = [...path, itemKey];
1303
- const itemData = data[index];
1304
- if (itemData !== undefined) {
1305
- markEntireStateAsServerSynced(
1306
- stateKey,
1307
- itemPath,
1308
- itemData,
1309
- timestamp
1310
- );
1311
- }
1312
- });
1313
- }
1314
- }
1315
- // If it's an object, mark each field as synced
1316
- else if (data && typeof data === 'object' && data.constructor === Object) {
1317
- Object.keys(data).forEach((key) => {
1318
- const fieldPath = [...path, key];
1319
- const fieldData = data[key];
1320
- markEntireStateAsServerSynced(stateKey, fieldPath, fieldData, timestamp);
1321
- });
1322
- }
1323
- }
1324
1237
  // 5. Batch queue
1325
1238
  let updateBatchQueue: any[] = [];
1326
1239
  let isFlushScheduled = false;
@@ -1508,12 +1421,8 @@ function handleUpdate(
1508
1421
  return null; // <-- Abort the update
1509
1422
  }
1510
1423
 
1511
- // ✅ FIX: The new `updateShadowAtPath` handles metadata preservation automatically.
1512
- // The manual loop has been removed.
1513
1424
  updateShadowAtPath(stateKey, path, newValue);
1514
1425
 
1515
- markAsDirty(stateKey, path, { bubble: true });
1516
-
1517
1426
  // Return the metadata of the node *after* the update.
1518
1427
  const newShadowMeta = getShadowMetadata(stateKey, path);
1519
1428
 
@@ -1534,10 +1443,8 @@ function handleInsertMany(
1534
1443
  shadowMeta: any;
1535
1444
  path: string[];
1536
1445
  } {
1537
- // Use the existing, optimized global store function to perform the state update
1538
1446
  insertManyShadowArrayElements(stateKey, path, payload);
1539
1447
 
1540
- markAsDirty(stateKey, path, { bubble: true });
1541
1448
  const updatedMeta = getShadowMetadata(stateKey, path);
1542
1449
 
1543
1450
  return {
@@ -1578,10 +1485,6 @@ function handleInsert(
1578
1485
  index,
1579
1486
  itemId
1580
1487
  );
1581
- //console.timeEnd('insertShadowArrayElement');
1582
-
1583
- markAsDirty(stateKey, path, { bubble: true });
1584
-
1585
1488
  const updatedMeta = getShadowMetadata(stateKey, path);
1586
1489
 
1587
1490
  let insertAfterId: string | undefined;
@@ -1606,7 +1509,6 @@ function handleCut(
1606
1509
  const parentArrayPath = path.slice(0, -1);
1607
1510
  const oldValue = getShadowValue(stateKey, path);
1608
1511
  removeShadowArrayElement(stateKey, path);
1609
- markAsDirty(stateKey, parentArrayPath, { bubble: true });
1610
1512
  return { type: 'cut', oldValue: oldValue, parentPath: parentArrayPath };
1611
1513
  }
1612
1514
 
@@ -1759,12 +1661,10 @@ export function useCogsStateFn<
1759
1661
  componentId,
1760
1662
  defaultState,
1761
1663
  dependencies,
1762
- serverState,
1763
1664
  }: {
1764
1665
  stateKey?: string;
1765
1666
  componentId?: string;
1766
1667
  defaultState?: TStateObject;
1767
- syncOptions?: SyncOptionsType<any>;
1768
1668
  } & OptionsType<TStateObject> = {}
1769
1669
  ): StateObject<TStateObject, TPlugins> {
1770
1670
  const [reactiveForce, forceUpdate] = useState({}); //this is the key to reactivity
@@ -1783,10 +1683,9 @@ export function useCogsStateFn<
1783
1683
  overrideOptions?: OptionsType<TStateObject>
1784
1684
  ): {
1785
1685
  value: TStateObject;
1786
- source: 'default' | 'server' | 'localStorage';
1686
+ source: 'default' | 'localStorage';
1787
1687
  timestamp: number;
1788
1688
  } => {
1789
- // If we pass in options, use them. Otherwise, get from the global store.
1790
1689
  const optionsToUse = overrideOptions
1791
1690
  ? { ...getInitialOptions(thisKey as string), ...overrideOptions }
1792
1691
  : getInitialOptions(thisKey as string);
@@ -1795,19 +1694,7 @@ export function useCogsStateFn<
1795
1694
  const finalDefaultState =
1796
1695
  currentOptions?.defaultState || defaultState || stateObject;
1797
1696
 
1798
- // 1. Check server state
1799
- const hasValidServerData =
1800
- currentOptions?.serverState?.status === 'success' &&
1801
- currentOptions?.serverState?.data !== undefined;
1802
-
1803
- if (hasValidServerData) {
1804
- return {
1805
- value: currentOptions.serverState!.data! as any,
1806
- source: 'server',
1807
- timestamp: currentOptions.serverState!.timestamp || Date.now(),
1808
- };
1809
- }
1810
- // 2. Check localStorage
1697
+ // Check localStorage
1811
1698
  if (currentOptions?.localStorage?.key && sessionId) {
1812
1699
  const localKey = isFunction(currentOptions.localStorage.key)
1813
1700
  ? currentOptions.localStorage.key(finalDefaultState)
@@ -1817,10 +1704,7 @@ export function useCogsStateFn<
1817
1704
  `${sessionId}-${thisKey}-${localKey}`
1818
1705
  );
1819
1706
 
1820
- if (
1821
- localData &&
1822
- localData.lastUpdated > (currentOptions?.serverState?.timestamp || 0)
1823
- ) {
1707
+ if (localData) {
1824
1708
  return {
1825
1709
  value: localData.state,
1826
1710
  source: 'localStorage',
@@ -1829,7 +1713,7 @@ export function useCogsStateFn<
1829
1713
  }
1830
1714
  }
1831
1715
 
1832
- // 3. Use default state
1716
+ // Use default state
1833
1717
  return {
1834
1718
  value: finalDefaultState || (stateObject as any),
1835
1719
  source: 'default',
@@ -1839,111 +1723,8 @@ export function useCogsStateFn<
1839
1723
  [thisKey, defaultState, stateObject, sessionId]
1840
1724
  );
1841
1725
 
1842
- // Effect 1: When this component's serverState prop changes, broadcast it
1843
- useEffect(() => {
1844
- if (!serverState) return;
1845
-
1846
- // Only broadcast if we have valid server data
1847
- if (serverState.status === 'success' && serverState.data !== undefined) {
1848
- setServerStateUpdate(thisKey, serverState);
1849
- }
1850
- }, [serverState, thisKey]);
1851
- // Effect 2: Listen for server state updates from ANY component
1726
+ // Removed: Server state effects are now owned by Shape Plugin
1852
1727
  useEffect(() => {
1853
- const unsubscribe = getGlobalStore
1854
- .getState()
1855
- .subscribeToPath(thisKey, (event) => {
1856
- if (event?.type === 'SERVER_STATE_UPDATE') {
1857
- const serverStateData = event.serverState;
1858
-
1859
- if (
1860
- serverStateData?.status !== 'success' ||
1861
- serverStateData.data === undefined
1862
- ) {
1863
- return; // Ignore if no valid data
1864
- }
1865
-
1866
- // Store the server state in options for future reference
1867
- setAndMergeOptions(thisKey, { serverState: serverStateData });
1868
-
1869
- const mergeConfig =
1870
- typeof serverStateData.merge === 'object'
1871
- ? serverStateData.merge
1872
- : serverStateData.merge === true
1873
- ? { strategy: 'append' as const, key: 'id' }
1874
- : null;
1875
-
1876
- const currentState = getShadowValue(thisKey, []);
1877
- const incomingData = serverStateData.data;
1878
-
1879
- if (
1880
- mergeConfig &&
1881
- mergeConfig.strategy === 'append' &&
1882
- 'key' in mergeConfig &&
1883
- Array.isArray(currentState) &&
1884
- Array.isArray(incomingData)
1885
- ) {
1886
- const keyField = mergeConfig.key;
1887
- if (!keyField) {
1888
- console.error(
1889
- "CogsState: Merge strategy 'append' requires a 'key' field."
1890
- );
1891
- return;
1892
- }
1893
-
1894
- // Get existing IDs to check for duplicates
1895
- const existingIds = new Set(
1896
- currentState.map((item: any) => item[keyField])
1897
- );
1898
-
1899
- // Filter out duplicates from incoming data
1900
- const newUniqueItems = incomingData.filter(
1901
- (item: any) => !existingIds.has(item[keyField])
1902
- );
1903
-
1904
- if (newUniqueItems.length > 0) {
1905
- // Insert only the new unique items
1906
- insertManyShadowArrayElements(thisKey, [], newUniqueItems);
1907
- }
1908
-
1909
- // Mark the entire merged state as synced
1910
- const finalState = getShadowValue(thisKey, []);
1911
- markEntireStateAsServerSynced(
1912
- thisKey,
1913
- [],
1914
- finalState,
1915
- serverStateData.timestamp || Date.now()
1916
- );
1917
- } else {
1918
- // Replace strategy (default) - completely replace the state
1919
- initializeShadowState(thisKey, incomingData);
1920
-
1921
- // Mark as synced from server
1922
- markEntireStateAsServerSynced(
1923
- thisKey,
1924
- [],
1925
- incomingData,
1926
- serverStateData.timestamp || Date.now()
1927
- );
1928
- }
1929
-
1930
- // Notify all components subscribed to this state
1931
- notifyComponents(thisKey);
1932
- }
1933
- });
1934
-
1935
- return unsubscribe;
1936
- }, [thisKey]);
1937
- useEffect(() => {
1938
- const existingMeta = getGlobalStore
1939
- .getState()
1940
- .getShadowMetadata(thisKey, []);
1941
-
1942
- // Skip if already initialized
1943
- if (existingMeta && existingMeta.stateSource) {
1944
- return;
1945
- }
1946
-
1947
1728
  const options = getInitialOptions(thisKey as string);
1948
1729
 
1949
1730
  const features = {
@@ -1951,7 +1732,6 @@ export function useCogsStateFn<
1951
1732
  };
1952
1733
 
1953
1734
  setShadowMetadata(thisKey, [], {
1954
- ...existingMeta,
1955
1735
  features,
1956
1736
  });
1957
1737
 
@@ -1964,7 +1744,7 @@ export function useCogsStateFn<
1964
1744
  }
1965
1745
  }
1966
1746
 
1967
- const { value: resolvedState, source, timestamp } = resolveInitialState();
1747
+ const { value: resolvedState } = resolveInitialState();
1968
1748
  initializeShadowState(thisKey, resolvedState);
1969
1749
 
1970
1750
  const validation = getInitialOptions(thisKey as string)?.validation;
@@ -1974,17 +1754,6 @@ export function useCogsStateFn<
1974
1754
  updateShadowTypeInfo(thisKey as string, validation.zodSchemaV3, 'zod3');
1975
1755
  }
1976
1756
 
1977
- setShadowMetadata(thisKey, [], {
1978
- stateSource: source,
1979
- lastServerSync: source === 'server' ? timestamp : undefined,
1980
- isDirty: source === 'server' ? false : undefined,
1981
- baseServerState: source === 'server' ? resolvedState : undefined,
1982
- });
1983
-
1984
- if (source === 'server' && serverState) {
1985
- setServerStateUpdate(thisKey, serverState);
1986
- }
1987
-
1988
1757
  notifyComponents(thisKey);
1989
1758
  }, [thisKey, ...(dependencies || [])]);
1990
1759
 
@@ -2089,7 +1858,6 @@ type MetaData = {
2089
1858
  fn: Function;
2090
1859
  path: string[]; // Which array this transform applies to
2091
1860
  }>;
2092
- serverStateIsUpStream?: boolean;
2093
1861
  };
2094
1862
 
2095
1863
  const applyTransforms = (
@@ -2254,11 +2022,11 @@ function getFieldElementsForPath(stateKey: string, path: string[]) {
2254
2022
  return refs.map((ref) => ref.current).filter(Boolean);
2255
2023
  }
2256
2024
 
2257
- function setFieldDisabledForPath(
2258
- stateKey: string,
2259
- path: string[],
2260
- disabled: boolean
2261
- ) {
2025
+ function setFieldDisabledForPath(
2026
+ stateKey: string,
2027
+ path: string[],
2028
+ disabled: boolean
2029
+ ) {
2262
2030
  getFieldElementsForPath(stateKey, path).forEach((el: any) => {
2263
2031
  if ('disabled' in el) {
2264
2032
  el.disabled = disabled;
@@ -2266,11 +2034,36 @@ function setFieldDisabledForPath(
2266
2034
  }
2267
2035
 
2268
2036
  el.style.pointerEvents = disabled ? 'none' : '';
2269
- el.setAttribute('aria-disabled', String(disabled));
2270
- });
2271
- }
2272
-
2273
- function createProxyHandler<
2037
+ el.setAttribute('aria-disabled', String(disabled));
2038
+ });
2039
+ }
2040
+
2041
+ function pluginMetaDependencyPath(
2042
+ path: string[],
2043
+ pluginName: string,
2044
+ scope = 'default'
2045
+ ) {
2046
+ return [...path, '$pluginMeta', pluginName, scope];
2047
+ }
2048
+
2049
+ function notifyPluginMetaDependency(
2050
+ stateKey: string,
2051
+ path: string[],
2052
+ pluginName: string,
2053
+ scope?: string
2054
+ ) {
2055
+ const rootMeta = getShadowMetadata(stateKey, []);
2056
+ const dependencyMeta = getShadowMetadata(
2057
+ stateKey,
2058
+ pluginMetaDependencyPath(path, pluginName, scope)
2059
+ );
2060
+
2061
+ dependencyMeta?.pathComponents?.forEach((componentId) => {
2062
+ rootMeta?.components?.get(componentId)?.forceUpdate();
2063
+ });
2064
+ }
2065
+
2066
+ function createProxyHandler<
2274
2067
  T,
2275
2068
  const TPlugins extends readonly CogsPlugin<
2276
2069
  any,
@@ -2395,9 +2188,19 @@ function createProxyHandler<
2395
2188
  });
2396
2189
  }
2397
2190
 
2191
+ const nextPath = [...path, prop];
2192
+ return rebuildStateShape({
2193
+ path: nextPath,
2194
+ componentId: componentId!,
2195
+ meta,
2196
+ });
2197
+ }
2198
+ if (typeof prop === 'string' && prop.startsWith('$')) {
2199
+ const methodName = prop.slice(1);
2200
+ const { value } = getScopedData(stateKey, path, meta);
2398
2201
  const registeredPlugins = pluginStore.getState().registeredPlugins;
2399
2202
  for (const plugin of registeredPlugins) {
2400
- const chainMethod = (plugin.chainMethods as any)?.[prop];
2203
+ const chainMethod = (plugin.chainMethods as any)?.[methodName];
2401
2204
  if (!chainMethod) continue;
2402
2205
  if (!pathMatchesPattern(path, chainMethod.pathPattern)) continue;
2403
2206
  if (!valueMatchesChainTarget(value, chainMethod.target)) continue;
@@ -2423,17 +2226,8 @@ function createProxyHandler<
2423
2226
  });
2424
2227
 
2425
2228
  return {
2426
- synced: () => {
2427
- const shadowMeta = getGlobalStore
2428
- .getState()
2429
- .getShadowMetadata(stateKey, path);
2430
-
2431
- setShadowMetadata(stateKey, path, {
2432
- ...shadowMeta,
2433
- isDirty: false,
2434
- stateSource: 'server',
2435
- lastServerSync: Date.now(),
2436
- });
2229
+ syned: () => {
2230
+ // Removed: sync status tracking is now owned by Shape Plugin
2437
2231
  },
2438
2232
  };
2439
2233
  },
@@ -2447,15 +2241,28 @@ function createProxyHandler<
2447
2241
  metaData,
2448
2242
  });
2449
2243
  },
2450
- getFieldMetaData: () =>
2451
- getPluginMetaDataMap(stateKey, path)?.get(plugin.name),
2452
- setFieldMetaData: (data: Record<string, any>) =>
2453
- setPluginMetaData(stateKey, path, plugin.name, data),
2454
- removeFieldMetaData: () =>
2455
- removePluginMetaData(stateKey, path, plugin.name),
2456
- getFieldRefs: () => getFieldRefsForPath(stateKey, path),
2457
- getFieldElements: () =>
2458
- getFieldElementsForPath(stateKey, path),
2244
+ getFieldMetaData: () =>
2245
+ getPluginMetaDataMap(stateKey, path)?.get(plugin.name),
2246
+ setFieldMetaData: (data: Record<string, any>) =>
2247
+ setPluginMetaData(stateKey, path, plugin.name, data),
2248
+ removeFieldMetaData: () =>
2249
+ removePluginMetaData(stateKey, path, plugin.name),
2250
+ watchPluginMeta: (scope?: string) =>
2251
+ registerComponentDependency(
2252
+ stateKey,
2253
+ componentId,
2254
+ pluginMetaDependencyPath(path, plugin.name, scope)
2255
+ ),
2256
+ notifyPluginMeta: (scope?: string) =>
2257
+ notifyPluginMetaDependency(
2258
+ stateKey,
2259
+ path,
2260
+ plugin.name,
2261
+ scope
2262
+ ),
2263
+ getFieldRefs: () => getFieldRefsForPath(stateKey, path),
2264
+ getFieldElements: () =>
2265
+ getFieldElementsForPath(stateKey, path),
2459
2266
  setFieldDisabled: (disabled: boolean) =>
2460
2267
  setFieldDisabledForPath(stateKey, path, disabled),
2461
2268
  },
@@ -2463,115 +2270,13 @@ function createProxyHandler<
2463
2270
  );
2464
2271
  };
2465
2272
  }
2466
-
2467
- const nextPath = [...path, prop];
2468
- return rebuildStateShape({
2469
- path: nextPath,
2470
- componentId: componentId!,
2471
- meta,
2472
- });
2473
2273
  }
2474
2274
  if (prop === '$_rebuildStateShape') {
2475
2275
  return rebuildStateShape;
2476
2276
  }
2477
2277
 
2478
- if (prop === '$sync' && path.length === 0) {
2479
- return async function () {
2480
- const options = getGlobalStore
2481
- .getState()
2482
- .getInitialOptions(stateKey);
2483
- const sync = options?.sync;
2484
-
2485
- if (!sync) {
2486
- console.error(`No mutation defined for state key "${stateKey}"`);
2487
- return { success: false, error: `No mutation defined` };
2488
- }
2489
-
2490
- const state = getGlobalStore
2491
- .getState()
2492
- .getShadowValue(stateKey, []);
2493
- const validationKey = options?.validation?.key;
2494
-
2495
- try {
2496
- const response = await sync.action(state);
2497
- if (
2498
- response &&
2499
- !response.success &&
2500
- response.errors &&
2501
- validationKey
2502
- ) {
2503
- // getGlobalStore.getState().removeValidationError(validationKey);
2504
- // response.errors.forEach((error) => {
2505
- // const errorPath = [validationKey, ...error.path].join('.');
2506
- // getGlobalStore
2507
- // .getState()
2508
- // .addValidationError(errorPath, error.message);
2509
- // });
2510
- // notifyComponents(stateKey);
2511
- }
2512
-
2513
- if (response?.success) {
2514
- // Mark as synced and not dirty
2515
- const shadowMeta = getGlobalStore
2516
- .getState()
2517
- .getShadowMetadata(stateKey, []);
2518
- setShadowMetadata(stateKey, [], {
2519
- ...shadowMeta,
2520
- isDirty: false,
2521
- lastServerSync: Date.now(),
2522
- stateSource: 'server',
2523
- baseServerState: state, // Update base server state
2524
- });
2525
-
2526
- if (sync.onSuccess) {
2527
- sync.onSuccess(response.data);
2528
- }
2529
- } else if (!response?.success && sync.onError)
2530
- sync.onError(response.error);
2531
-
2532
- return response;
2533
- } catch (error) {
2534
- if (sync.onError) sync.onError(error);
2535
- return { success: false, error };
2536
- }
2537
- };
2538
- }
2539
- // Fixed getStatus function in createProxyHandler
2540
- if (prop === '$_status' || prop === '$getStatus') {
2541
- const getStatusFunc = () => {
2542
- // ✅ Use the optimized helper to get all data in one efficient call
2543
- const { shadowMeta, value } = getScopedData(stateKey, path, meta);
2544
-
2545
- if (shadowMeta?.isDirty === true) {
2546
- return 'dirty';
2547
- }
2548
-
2549
- if (
2550
- shadowMeta?.stateSource === 'server' ||
2551
- shadowMeta?.isDirty === false
2552
- ) {
2553
- return 'synced';
2554
- }
2555
-
2556
- if (shadowMeta?.stateSource === 'localStorage') {
2557
- return 'restored';
2558
- }
2559
-
2560
- if (shadowMeta?.stateSource === 'default') {
2561
- return 'fresh';
2562
- }
2563
-
2564
- if (value !== undefined) {
2565
- return 'fresh';
2566
- }
2567
-
2568
- // Fallback if no other condition is met.
2569
- return 'unknown';
2570
- };
2571
-
2572
- // This part remains the same
2573
- return prop === '$_status' ? getStatusFunc() : getStatusFunc;
2574
- }
2278
+ // Removed: $sync is now owned by Shape Plugin
2279
+ // Removed: $_status and $getStatus are now owned by Shape Plugin
2575
2280
  if (prop === '$removeStorage') {
2576
2281
  return () => {
2577
2282
  const initialState =
@@ -2593,75 +2298,75 @@ function createProxyHandler<
2593
2298
  setShadowMetadata(stateKey, path, { ...currentMeta, isRaw: true });
2594
2299
  effectiveSetState(value, path, { updateType: 'update' });
2595
2300
  };
2596
- }
2597
-
2598
- if (prop === '$validate') {
2599
- return (keys?: readonly string[]) => {
2600
- const store = getGlobalStore.getState();
2601
-
2602
- if (keys) {
2603
- const results = keys.map((key) => {
2604
- const targetPath = [...path, key];
2605
- const targetValue = store.getShadowValue(stateKey, targetPath);
2606
- const currentMeta =
2607
- store.getShadowMetadata(stateKey, targetPath) || {};
2608
- const fieldSchema = currentMeta.typeInfo?.schema;
2609
-
2610
- if (!fieldSchema) {
2611
- return {
2612
- key,
2613
- path: targetPath,
2614
- success: true,
2615
- data: targetValue,
2616
- };
2617
- }
2618
-
2619
- const result = (fieldSchema as any).safeParse(targetValue);
2620
- const zodErrors =
2621
- result.error?.issues || result.error?.errors || [];
2622
-
2623
- store.setShadowMetadata(stateKey, targetPath, {
2624
- ...currentMeta,
2625
- validation: {
2626
- status: result.success ? 'VALID' : 'INVALID',
2627
- errors: result.success
2628
- ? []
2629
- : [
2630
- {
2631
- source: 'client',
2632
- message:
2633
- zodErrors[0]?.message || 'Invalid value',
2634
- severity: 'error',
2635
- code: zodErrors[0]?.code,
2636
- },
2637
- ],
2638
- lastValidated: Date.now(),
2639
- validatedValue: targetValue,
2640
- },
2641
- });
2642
- store.notifyPathSubscribers([stateKey, ...targetPath].join('.'), {
2643
- type: 'VALIDATION_UPDATE',
2644
- });
2645
-
2646
- return {
2647
- key,
2648
- path: targetPath,
2649
- success: result.success,
2650
- data: result.success ? result.data : undefined,
2651
- error: result.success ? undefined : result.error,
2652
- };
2653
- });
2654
-
2655
- notifyComponents(stateKey);
2656
-
2657
- return {
2658
- success: results.every((result) => result.success),
2659
- results,
2660
- };
2661
- }
2662
-
2663
- // 1. Get Data & Schema
2664
- const { value } = getScopedData(stateKey, path, meta);
2301
+ }
2302
+
2303
+ if (prop === '$validate') {
2304
+ return (keys?: readonly string[]) => {
2305
+ const store = getGlobalStore.getState();
2306
+
2307
+ if (keys) {
2308
+ const results = keys.map((key) => {
2309
+ const targetPath = [...path, key];
2310
+ const targetValue = store.getShadowValue(stateKey, targetPath);
2311
+ const currentMeta =
2312
+ store.getShadowMetadata(stateKey, targetPath) || {};
2313
+ const fieldSchema = currentMeta.typeInfo?.schema;
2314
+
2315
+ if (!fieldSchema) {
2316
+ return {
2317
+ key,
2318
+ path: targetPath,
2319
+ success: true,
2320
+ data: targetValue,
2321
+ };
2322
+ }
2323
+
2324
+ const result = (fieldSchema as any).safeParse(targetValue);
2325
+ const zodErrors =
2326
+ result.error?.issues || result.error?.errors || [];
2327
+
2328
+ store.setShadowMetadata(stateKey, targetPath, {
2329
+ ...currentMeta,
2330
+ validation: {
2331
+ status: result.success ? 'VALID' : 'INVALID',
2332
+ errors: result.success
2333
+ ? []
2334
+ : [
2335
+ {
2336
+ source: 'client',
2337
+ message:
2338
+ zodErrors[0]?.message || 'Invalid value',
2339
+ severity: 'error',
2340
+ code: zodErrors[0]?.code,
2341
+ },
2342
+ ],
2343
+ lastValidated: Date.now(),
2344
+ validatedValue: targetValue,
2345
+ },
2346
+ });
2347
+ store.notifyPathSubscribers([stateKey, ...targetPath].join('.'), {
2348
+ type: 'VALIDATION_UPDATE',
2349
+ });
2350
+
2351
+ return {
2352
+ key,
2353
+ path: targetPath,
2354
+ success: result.success,
2355
+ data: result.success ? result.data : undefined,
2356
+ error: result.success ? undefined : result.error,
2357
+ };
2358
+ });
2359
+
2360
+ notifyComponents(stateKey);
2361
+
2362
+ return {
2363
+ success: results.every((result) => result.success),
2364
+ results,
2365
+ };
2366
+ }
2367
+
2368
+ // 1. Get Data & Schema
2369
+ const { value } = getScopedData(stateKey, path, meta);
2665
2370
  const opts = store.getInitialOptions(stateKey);
2666
2371
  const schema =
2667
2372
  opts?.validation?.zodSchemaV4 || opts?.validation?.zodSchemaV3;
@@ -2719,11 +2424,11 @@ function createProxyHandler<
2719
2424
  return result;
2720
2425
  };
2721
2426
  }
2722
- if (prop === '$showValidationErrors') {
2723
- return () => {
2724
- const { shadowMeta } = getScopedData(stateKey, path, meta);
2725
- if (
2726
- shadowMeta?.validation?.status === 'INVALID' &&
2427
+ if (prop === '$showValidationErrors') {
2428
+ return () => {
2429
+ const { shadowMeta } = getScopedData(stateKey, path, meta);
2430
+ if (
2431
+ shadowMeta?.validation?.status === 'INVALID' &&
2727
2432
  shadowMeta.validation.errors.length > 0
2728
2433
  ) {
2729
2434
  // Return only error-severity messages (not warnings)
@@ -2731,55 +2436,55 @@ function createProxyHandler<
2731
2436
  .filter((err) => err.severity === 'error')
2732
2437
  .map((err) => err.message);
2733
2438
  }
2734
- return [];
2735
- };
2736
- }
2737
- if (prop === '$validationErrors') {
2738
- return (keys?: readonly string[]) => {
2739
- const store = getGlobalStore.getState();
2740
- const summarizePath = (targetPath: string[]) => {
2741
- const validationState =
2742
- store.getShadowMetadata(stateKey, targetPath)?.validation;
2743
- const allErrors = (validationState?.errors || []).map((err) => ({
2744
- ...err,
2745
- path: targetPath,
2746
- }));
2747
- const errors = allErrors.filter((err) => err.severity === 'error');
2748
- const warnings = allErrors.filter(
2749
- (err) => err.severity === 'warning'
2750
- );
2751
- const severity: ValidationSeverity =
2752
- errors.length > 0
2753
- ? 'error'
2754
- : warnings.length > 0
2755
- ? 'warning'
2756
- : undefined;
2757
-
2758
- return {
2759
- status: validationState?.status || 'NOT_VALIDATED',
2760
- severity,
2761
- hasErrors: errors.length > 0,
2762
- hasWarnings: warnings.length > 0,
2763
- message: errors[0]?.message || warnings[0]?.message || '',
2764
- errors: errors.map((err) => err.message),
2765
- warnings: warnings.map((err) => err.message),
2766
- allErrors,
2767
- path: targetPath,
2768
- getData: () => store.getShadowValue(stateKey, targetPath),
2769
- } satisfies ValidationFieldSummary;
2770
- };
2771
-
2772
- const childKeys =
2773
- keys ??
2774
- Object.keys(store.getShadowNode(stateKey, path) ?? {}).filter(
2775
- (key) => key !== '_meta'
2776
- );
2777
-
2778
- return childKeys.map((key) => summarizePath([...path, key]));
2779
- };
2780
- }
2781
-
2782
- if (prop === '$getSelected') {
2439
+ return [];
2440
+ };
2441
+ }
2442
+ if (prop === '$validationErrors') {
2443
+ return (keys?: readonly string[]) => {
2444
+ const store = getGlobalStore.getState();
2445
+ const summarizePath = (targetPath: string[]) => {
2446
+ const validationState =
2447
+ store.getShadowMetadata(stateKey, targetPath)?.validation;
2448
+ const allErrors = (validationState?.errors || []).map((err) => ({
2449
+ ...err,
2450
+ path: targetPath,
2451
+ }));
2452
+ const errors = allErrors.filter((err) => err.severity === 'error');
2453
+ const warnings = allErrors.filter(
2454
+ (err) => err.severity === 'warning'
2455
+ );
2456
+ const severity: ValidationSeverity =
2457
+ errors.length > 0
2458
+ ? 'error'
2459
+ : warnings.length > 0
2460
+ ? 'warning'
2461
+ : undefined;
2462
+
2463
+ return {
2464
+ status: validationState?.status || 'NOT_VALIDATED',
2465
+ severity,
2466
+ hasErrors: errors.length > 0,
2467
+ hasWarnings: warnings.length > 0,
2468
+ message: errors[0]?.message || warnings[0]?.message || '',
2469
+ errors: errors.map((err) => err.message),
2470
+ warnings: warnings.map((err) => err.message),
2471
+ allErrors,
2472
+ path: targetPath,
2473
+ getData: () => store.getShadowValue(stateKey, targetPath),
2474
+ } satisfies ValidationFieldSummary;
2475
+ };
2476
+
2477
+ const childKeys =
2478
+ keys ??
2479
+ Object.keys(store.getShadowNode(stateKey, path) ?? {}).filter(
2480
+ (key) => key !== '_meta'
2481
+ );
2482
+
2483
+ return childKeys.map((key) => summarizePath([...path, key]));
2484
+ };
2485
+ }
2486
+
2487
+ if (prop === '$getSelected') {
2783
2488
  return () => {
2784
2489
  const arrayKey = [stateKey, ...path].join('.');
2785
2490
  registerComponentDependency(stateKey, componentId, [
@@ -3045,9 +2750,7 @@ function createProxyHandler<
3045
2750
  e.type === 'INSERT' ||
3046
2751
  e.type === 'INSERT_MANY' ||
3047
2752
  e.type === 'REMOVE' ||
3048
- e.type === 'CLEAR_SELECTION' ||
3049
- (e.type === 'SERVER_STATE_UPDATE' &&
3050
- !meta?.serverStateIsUpStream)
2753
+ e.type === 'CLEAR_SELECTION'
3051
2754
  ) {
3052
2755
  forceUpdate({});
3053
2756
  }
@@ -3384,10 +3087,6 @@ function createProxyHandler<
3384
3087
  $cogsSignal({ _stateKey: stateKey, _path: path, _meta: meta });
3385
3088
  }
3386
3089
 
3387
- if (prop === '$lastSynced') {
3388
- const syncKey = `${stateKey}:${path.join('.')}`;
3389
- return getSyncInfo(syncKey);
3390
- }
3391
3090
  if (prop == 'getLocalStorage') {
3392
3091
  return (key: string) =>
3393
3092
  loadFromLocalStorage(sessionId + '-' + stateKey + '-' + key);
@@ -3659,8 +3358,6 @@ function createProxyHandler<
3659
3358
  };
3660
3359
  store.updateShadowAtPath(stateKey, relativePath, value);
3661
3360
 
3662
- store.markAsDirty(stateKey, relativePath, { bubble: true });
3663
-
3664
3361
  // Bubble up - notify components at this path and all parent paths
3665
3362
  let currentPath = [...relativePath];
3666
3363
  while (true) {
@@ -3690,7 +3387,6 @@ function createProxyHandler<
3690
3387
  case 'remove': {
3691
3388
  const parentPath = relativePath.slice(0, -1);
3692
3389
  store.removeShadowArrayElement(stateKey, relativePath);
3693
- store.markAsDirty(stateKey, parentPath, { bubble: true });
3694
3390
 
3695
3391
  // Bubble up from parent path
3696
3392
  let currentPath = [...parentPath];
@@ -3754,22 +3450,7 @@ function createProxyHandler<
3754
3450
 
3755
3451
  return {
3756
3452
  synced: () => {
3757
- const shadowMeta = getGlobalStore
3758
- .getState()
3759
- .getShadowMetadata(stateKey, path);
3760
-
3761
- setShadowMetadata(stateKey, path, {
3762
- ...shadowMeta,
3763
- isDirty: false,
3764
- stateSource: 'server',
3765
- lastServerSync: Date.now(),
3766
- });
3767
-
3768
- const fullPath = [stateKey, ...path].join('.');
3769
- notifyPathSubscribers(fullPath, {
3770
- type: 'SYNC_STATUS_CHANGE',
3771
- isDirty: false,
3772
- });
3453
+ // Removed: sync status tracking is now owned by Shape Plugin
3773
3454
  },
3774
3455
  };
3775
3456
  };
@@ -3852,16 +3533,8 @@ function createProxyHandler<
3852
3533
  // ... (rest of the function: rootLevelMethods, returnShape, etc.)
3853
3534
  const rootLevelMethods = {
3854
3535
  $revertToInitialState: (obj?: { validationKey?: string }) => {
3855
- const shadowMeta = getGlobalStore
3856
- .getState()
3857
- .getShadowMetadata(stateKey, []);
3858
- let revertState;
3859
-
3860
- if (shadowMeta?.stateSource === 'server' && shadowMeta.baseServerState) {
3861
- revertState = shadowMeta.baseServerState;
3862
- } else {
3863
- revertState = getGlobalStore.getState().initialStateGlobal[stateKey];
3864
- }
3536
+ const revertState =
3537
+ getGlobalStore.getState().initialStateGlobal[stateKey];
3865
3538
 
3866
3539
  clearSelectedIndexesForState(stateKey);
3867
3540
  initializeShadowState(stateKey, revertState);