cogsbox-state 0.5.474 → 0.5.475-canary.0

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
@@ -44,32 +44,12 @@ import { useCogsConfig } from './CogsStateClient.js';
44
44
  import { Operation } from 'fast-json-patch';
45
45
 
46
46
  import * as z3 from 'zod/v3';
47
- import * as z4 from 'zod/v4';
48
47
 
49
48
  import { runValidation } from './validation';
49
+ import { ZodType } from 'zod/v4';
50
50
 
51
51
  export type Prettify<T> = T extends any ? { [K in keyof T]: T[K] } : never;
52
52
 
53
- export type VirtualViewOptions = {
54
- itemHeight?: number;
55
- overscan?: number;
56
- stickToBottom?: boolean;
57
- dependencies?: any[];
58
- scrollStickTolerance?: number;
59
- };
60
-
61
- // The result now returns a real StateObject
62
- export type VirtualStateObjectResult<T extends any[]> = {
63
- virtualState: StateObject<T>;
64
- virtualizerProps: {
65
- outer: { ref: RefObject<HTMLDivElement>; style: CSSProperties };
66
- inner: { style: CSSProperties };
67
- list: { style: CSSProperties };
68
- };
69
- scrollToBottom: (behavior?: ScrollBehavior) => void;
70
- scrollToIndex: (index: number, behavior?: ScrollBehavior) => void;
71
- };
72
-
73
53
  export type SyncInfo = {
74
54
  timeStamp: number;
75
55
  userId: number;
@@ -123,7 +103,10 @@ export type StreamHandle<T> = {
123
103
  resume: () => void;
124
104
  };
125
105
 
126
- export type ArrayEndType<TShape extends unknown> = {
106
+ export type ArrayEndType<
107
+ TShape extends unknown,
108
+ TPlugins extends readonly CogsPlugin<any, any, any, any, any>[],
109
+ > = {
127
110
  $stream: <T = Prettify<InferArrayElement<TShape>>, R = T>(
128
111
  options?: StreamOptions<T, R>
129
112
  ) => StreamHandle<T>;
@@ -141,24 +124,21 @@ export type ArrayEndType<TShape extends unknown> = {
141
124
  $cutSelected: () => void;
142
125
  $cutByValue: (value: string | number | boolean) => void;
143
126
  $toggleByValue: (value: string | number | boolean) => void;
144
- $stateSort: (
127
+ $sort: (
145
128
  compareFn: (
146
129
  a: Prettify<InferArrayElement<TShape>>,
147
130
  b: Prettify<InferArrayElement<TShape>>
148
131
  ) => number
149
- ) => ArrayEndType<TShape>;
150
- $useVirtualView: (
151
- options: VirtualViewOptions
152
- ) => VirtualStateObjectResult<Prettify<InferArrayElement<TShape>>[]>;
132
+ ) => ArrayEndType<TShape, TPlugins>;
153
133
 
154
- $stateList: (
134
+ $list: (
155
135
  callbackfn: (
156
136
  setter: StateObject<Prettify<InferArrayElement<TShape>>>,
157
137
  index: number,
158
138
  arraySetter: StateObject<TShape>
159
139
  ) => void
160
140
  ) => any;
161
- $stateMap: <U>(
141
+ $map: <U>(
162
142
  callbackfn: (
163
143
  setter: StateObject<Prettify<InferArrayElement<TShape>>>,
164
144
  index: number,
@@ -174,18 +154,18 @@ export type ArrayEndType<TShape extends unknown> = {
174
154
  fields?: (keyof Prettify<InferArrayElement<TShape>>)[],
175
155
  onMatch?: (existingItem: any) => any
176
156
  ) => void;
177
- $stateFind: (
157
+ $find: (
178
158
  callbackfn: (
179
159
  value: Prettify<InferArrayElement<TShape>>,
180
160
  index: number
181
161
  ) => boolean
182
162
  ) => StateObject<Prettify<InferArrayElement<TShape>>> | undefined;
183
- $stateFilter: (
163
+ $filter: (
184
164
  callbackfn: (
185
165
  value: Prettify<InferArrayElement<TShape>>,
186
166
  index: number
187
167
  ) => void
188
- ) => ArrayEndType<TShape>;
168
+ ) => ArrayEndType<TShape, TPlugins>;
189
169
  $getSelected: () =>
190
170
  | StateObject<Prettify<InferArrayElement<TShape>>>
191
171
  | undefined;
@@ -221,7 +201,17 @@ export type InsertType<T> = (payload: InsertParams<T>, index?: number) => void;
221
201
  export type InsertTypeObj<T> = (payload: InsertParams<T>) => void;
222
202
 
223
203
  type EffectFunction<T, R> = (state: T, deps: any[]) => R;
224
- export type EndType<T, IsArrayElement = false> = {
204
+ export type PerPathFormOptsType<
205
+ TState,
206
+ TPlugins extends readonly CogsPlugin<any, any, any, any, any>[] = [],
207
+ > = Omit<FormOptsType, 'formElements'> & {
208
+ formElements?: FormsElementsType<TState, TPlugins>;
209
+ };
210
+ export type EndType<
211
+ T,
212
+ TPlugins extends readonly CogsPlugin<any, any, any, any, any>[] = [],
213
+ IsArrayElement = false,
214
+ > = {
225
215
  $getPluginMetaData: (pluginName: string) => Record<string, any>;
226
216
  $addPluginMetaData: (key: string, data: Record<string, any>) => void;
227
217
  $removePluginMetaData: (key: string) => void;
@@ -241,18 +231,22 @@ export type EndType<T, IsArrayElement = false> = {
241
231
  $_path: string[];
242
232
  $_stateKey: string;
243
233
  $isolate: (
244
- renderFn: (state: StateObject<T>) => React.ReactNode
234
+ renderFn: (state: StateObject<T, TPlugins>) => React.ReactNode
235
+ ) => JSX.Element;
236
+ $formElement: (
237
+ control: FormControl<T>,
238
+ opts?: PerPathFormOptsType<T, TPlugins>
245
239
  ) => JSX.Element;
246
- $formElement: (control: FormControl<T>, opts?: FormOptsType) => JSX.Element;
247
240
  $get: () => T;
248
241
  $$get: () => T;
249
242
  $$derive: <R>(fn: EffectFunction<T, R>) => R;
250
243
  $_status: 'fresh' | 'dirty' | 'synced' | 'restored' | 'unknown';
251
244
  $getStatus: () => 'fresh' | 'dirty' | 'synced' | 'restored' | 'unknown';
252
245
  $showValidationErrors: () => string[];
246
+ $validate: () => { success: boolean; data?: T; error?: any };
253
247
  $setValidation: (ctx: string) => void;
254
248
  $removeValidation: (ctx: string) => void;
255
- $ignoreFields: (fields: string[]) => StateObject<T>;
249
+
256
250
  $isSelected: boolean;
257
251
  $setSelected: (value: boolean) => void;
258
252
  $toggleSelected: () => void;
@@ -276,14 +270,22 @@ export type EndType<T, IsArrayElement = false> = {
276
270
  $lastSynced?: SyncInfo;
277
271
  } & (IsArrayElement extends true ? { $cutThis: () => void } : {});
278
272
 
279
- export type StateObject<T> = (T extends any[]
280
- ? ArrayEndType<T>
273
+ export type StateObject<
274
+ T,
275
+ TPlugins extends readonly CogsPlugin<any, any, any, any, any>[] = [],
276
+ > = {
277
+ // A. Callable Getter: state.count()
278
+ (): T;
279
+ // B. Callable Setter: state.count(5)
280
+ (newValue: T | ((prev: T) => T)): void;
281
+ } & (T extends any[]
282
+ ? ArrayEndType<T, TPlugins>
281
283
  : T extends Record<string, unknown> | object
282
- ? { [K in keyof T]-?: StateObject<T[K]> }
284
+ ? { [K in keyof T]-?: StateObject<T[K], TPlugins> }
283
285
  : T extends string | number | boolean | null
284
- ? EndType<T, true>
286
+ ? EndType<T, TPlugins, true>
285
287
  : never) &
286
- EndType<T, true> & {
288
+ EndType<T, TPlugins, true> & {
287
289
  $toggle: T extends boolean ? () => void : never;
288
290
 
289
291
  $_componentId: string | null;
@@ -365,7 +367,7 @@ export type ReactivityType =
365
367
  type ValidationOptionsType = {
366
368
  key?: string;
367
369
  zodSchemaV3?: z3.ZodType<any, any, any>;
368
- zodSchemaV4?: z4.ZodType<any, any, any>;
370
+ zodSchemaV4?: ZodType;
369
371
  onBlur?: 'error' | 'warning';
370
372
  onChange?: 'error' | 'warning';
371
373
  blockSync?: boolean;
@@ -439,7 +441,12 @@ export type OptionsType<
439
441
 
440
442
  dependencies?: any[];
441
443
  };
442
-
444
+ type ScopedPluginApi<THookReturn, TFieldMetaData> = {
445
+ hookData: THookReturn;
446
+ getFieldMetaData: () => TFieldMetaData | undefined;
447
+ setFieldMetaData: (data: Partial<TFieldMetaData>) => void;
448
+ // Add any other scoped functions or data a plugin might need here
449
+ };
443
450
  export type FormsElementsType<
444
451
  TState,
445
452
  TPlugins extends readonly CogsPlugin<any, any, any, any, any>[] = [],
@@ -455,6 +462,19 @@ export type FormsElementsType<
455
462
  path: string[];
456
463
  message?: string;
457
464
  getData?: () => TState;
465
+ plugins: {
466
+ // It maps over the plugins...
467
+ [P in TPlugins[number] as P['name']]: P extends CogsPlugin<
468
+ any,
469
+ any,
470
+ infer THookReturn,
471
+ any,
472
+ infer TFieldMetaData
473
+ >
474
+ ? // ...and provides the scoped API for each one.
475
+ ScopedPluginApi<THookReturn, TFieldMetaData>
476
+ : never;
477
+ };
458
478
  }) => React.ReactNode;
459
479
  syncRender?: (options: {
460
480
  children: React.ReactNode;
@@ -462,16 +482,6 @@ export type FormsElementsType<
462
482
  data?: TState;
463
483
  key?: string;
464
484
  }) => React.ReactNode;
465
- } & {
466
- // For each plugin `P` in the TPlugins array...
467
- [P in TPlugins[number] as P['name']]?: P['formWrapper'] extends (
468
- // ...check if its `formWrapper` property is a function...
469
- arg: any
470
- ) => any
471
- ? // ...if it is, infer the type of its FIRST PARAMETER. This is the key.
472
- Parameters<P['formWrapper']>[0]
473
- : // Otherwise, the type is invalid.
474
- never;
475
485
  };
476
486
  export type CogsInitialState<T> =
477
487
  | {
@@ -796,18 +806,21 @@ export const createCogsState = <
796
806
  const thiState =
797
807
  getShadowValue(stateKey as string, []) || statePart[stateKey as string];
798
808
 
799
- const updater = useCogsStateFn<(typeof statePart)[StateKey]>(thiState, {
800
- stateKey: stateKey as string,
801
- syncUpdate: options?.syncUpdate,
802
- componentId,
803
- localStorage: options?.localStorage,
804
- middleware: options?.middleware,
805
- reactiveType: options?.reactiveType,
806
- reactiveDeps: options?.reactiveDeps,
807
- defaultState: options?.defaultState as any,
808
- dependencies: options?.dependencies,
809
- serverState: options?.serverState,
810
- });
809
+ const updater = useCogsStateFn<(typeof statePart)[StateKey], TPlugins>(
810
+ thiState,
811
+ {
812
+ stateKey: stateKey as string,
813
+ syncUpdate: options?.syncUpdate,
814
+ componentId,
815
+ localStorage: options?.localStorage,
816
+ middleware: options?.middleware,
817
+ reactiveType: options?.reactiveType,
818
+ reactiveDeps: options?.reactiveDeps,
819
+ defaultState: options?.defaultState as any,
820
+ dependencies: options?.dependencies,
821
+ serverState: options?.serverState,
822
+ }
823
+ );
811
824
 
812
825
  useEffect(() => {
813
826
  if (options) {
@@ -831,62 +844,58 @@ export const createCogsState = <
831
844
 
832
845
  function setCogsOptionsByKey<StateKey extends StateKeys>(
833
846
  stateKey: StateKey,
834
- options: OptionsType<(typeof statePart)[StateKey]>
847
+ options: CreateStateOptionsType<(typeof statePart)[StateKey], TPlugins> &
848
+ Omit<
849
+ OptionsType<(typeof statePart)[StateKey]>,
850
+ keyof CreateStateOptionsType
851
+ >
835
852
  ) {
836
- setOptions({ stateKey, options, initialOptionsPart });
853
+ setOptions({ stateKey, options, initialOptionsPart } as any);
837
854
 
838
855
  if (options.localStorage) {
839
856
  loadAndApplyLocalStorage(stateKey as string, options);
840
857
  }
858
+ if (options.formElements) {
859
+ const currentPlugins = pluginStore.getState().registeredPlugins;
841
860
 
861
+ const updatedPlugins = currentPlugins.map((plugin) => {
862
+ // Use `options.formElements` as the source
863
+ if (options.formElements!.hasOwnProperty(plugin.name)) {
864
+ return {
865
+ ...plugin,
866
+ formWrapper: (options.formElements as any)[plugin.name],
867
+ };
868
+ }
869
+ return plugin;
870
+ });
871
+
872
+ pluginStore.getState().setRegisteredPlugins(updatedPlugins as any);
873
+ }
842
874
  notifyComponents(stateKey as string);
843
875
  }
844
-
845
- function setCogsFormElements(
846
- formElements: FormsElementsType<State, TPlugins>
876
+ function setCogsOptions(
877
+ // The type allows any valid options, but uses a generic `unknown` for the
878
+ // state type because it applies to multiple different state shapes.
879
+ // The `TPlugins` generic is correctly preserved.
880
+ globalOptions: CreateStateOptionsType<unknown, TPlugins> &
881
+ Omit<OptionsType<unknown>, keyof CreateStateOptionsType>
847
882
  ) {
848
- // Get the current list of registered plugins from the store.
849
- const currentPlugins = pluginStore.getState().registeredPlugins;
850
-
851
- // Create a new array by mapping over the current plugins.
852
- // This is crucial for immutability and ensuring Zustand detects the change.
853
- const updatedPlugins = currentPlugins.map((plugin) => {
854
- // Check if the formElements object has a wrapper for this specific plugin by name.
855
- if (formElements.hasOwnProperty(plugin.name)) {
856
- // If it does, return a *new* plugin object.
857
- // Spread the existing plugin properties and add/overwrite the formWrapper.
858
- return {
859
- ...plugin,
860
- formWrapper: formElements[plugin.name as keyof typeof formElements],
861
- };
862
- }
863
- // If there's no new wrapper for this plugin, return the original object.
864
- return plugin;
865
- });
866
-
867
- // Use the store's dedicated setter function to update the registered plugins list.
868
- // This will trigger a state update that components listening to the store will react to.
869
- pluginStore.getState().setRegisteredPlugins(updatedPlugins as any);
883
+ // Get all the state keys that this instance manages
884
+ const allStateKeys = Object.keys(statePart) as StateKeys[];
870
885
 
871
- // For good measure and consistency, we should still update the formElements
872
- // in the initial options, in case any other part of the system relies on it.
873
- const allStateKeys = Object.keys(statePart);
886
+ // Loop through every state key and apply the provided options
874
887
  allStateKeys.forEach((key) => {
875
- const existingOptions = getInitialOptions(key) || {};
876
- const finalOptions = {
877
- ...existingOptions,
878
- formElements: {
879
- ...(existingOptions.formElements || {}),
880
- ...formElements,
881
- },
882
- };
883
- setInitialStateOptions(key, finalOptions);
888
+ // We use `as any` here because we are intentionally applying a single
889
+ // generic options object to many differently-typed state slices.
890
+ // The internal logic of setCogsOptionsByKey handles the merging correctly.
891
+ setCogsOptionsByKey(key, globalOptions as any);
884
892
  });
885
893
  }
894
+
886
895
  return {
887
896
  useCogsState,
888
897
  setCogsOptionsByKey,
889
- setCogsFormElements,
898
+ setCogsOptions,
890
899
  };
891
900
  };
892
901
 
@@ -1233,11 +1242,14 @@ function handleUpdate(
1233
1242
  stateKey: string,
1234
1243
  path: string[],
1235
1244
  payload: any
1236
- ): { type: 'update'; oldValue: any; newValue: any; shadowMeta: any } {
1245
+ ): { type: 'update'; oldValue: any; newValue: any; shadowMeta: any } | null {
1237
1246
  // ✅ FIX: Get the old value before the update.
1238
1247
  const oldValue = getGlobalStore.getState().getShadowValue(stateKey, path);
1239
1248
 
1240
1249
  const newValue = isFunction(payload) ? payload(oldValue) : payload;
1250
+ if (isDeepEqual(oldValue, newValue)) {
1251
+ return null; // <-- Abort the update
1252
+ }
1241
1253
 
1242
1254
  // ✅ FIX: The new `updateShadowAtPath` handles metadata preservation automatically.
1243
1255
  // The manual loop has been removed.
@@ -1427,6 +1439,10 @@ function createEffectiveSetState<T>(
1427
1439
  result = handleCut(stateKey, path);
1428
1440
  break;
1429
1441
  }
1442
+
1443
+ if (result === null) {
1444
+ return;
1445
+ }
1430
1446
  result.stateKey = stateKey;
1431
1447
  result.path = path;
1432
1448
  updateBatchQueue.push(result);
@@ -1464,7 +1480,10 @@ function createEffectiveSetState<T>(
1464
1480
  }
1465
1481
  }
1466
1482
 
1467
- export function useCogsStateFn<TStateObject extends unknown>(
1483
+ export function useCogsStateFn<
1484
+ TStateObject extends unknown,
1485
+ const TPlugins extends readonly CogsPlugin<any, any, any, any, any>[],
1486
+ >(
1468
1487
  stateObject: TStateObject,
1469
1488
  {
1470
1489
  stateKey,
@@ -1774,7 +1793,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
1774
1793
  }
1775
1794
 
1776
1795
  const updaterFinal = useMemo(() => {
1777
- const handler = createProxyHandler<TStateObject>(
1796
+ const handler = createProxyHandler<TStateObject, TPlugins>(
1778
1797
  thisKey,
1779
1798
  effectiveSetState,
1780
1799
  componentIdRef.current,
@@ -1923,12 +1942,15 @@ function getScopedData(stateKey: string, path: string[], meta?: MetaData) {
1923
1942
  };
1924
1943
  }
1925
1944
 
1926
- function createProxyHandler<T>(
1945
+ function createProxyHandler<
1946
+ T,
1947
+ const TPlugins extends readonly CogsPlugin<any, any, any, any, any>[],
1948
+ >(
1927
1949
  stateKey: string,
1928
1950
  effectiveSetState: EffectiveSetState<T>,
1929
1951
  outerComponentId: string,
1930
1952
  sessionId?: string
1931
- ): StateObject<T> {
1953
+ ): StateObject<T, TPlugins> {
1932
1954
  const proxyCache = new Map<string, any>();
1933
1955
 
1934
1956
  function rebuildStateShape({
@@ -1951,15 +1973,31 @@ function createProxyHandler<T>(
1951
1973
  }
1952
1974
  const stateKeyPathKey = [stateKey, ...path].join('.');
1953
1975
 
1954
- // We attach baseObj properties *inside* the get trap now to avoid recursion
1955
- // This is a placeholder for the proxy.
1976
+ const proxyTarget = () => {};
1956
1977
 
1957
1978
  const handler = {
1958
- get(target: any, prop: string) {
1979
+ apply(target: any, thisArg: any, args: any) {
1980
+ if (args.length === 0) {
1981
+ // FIX: Calculate viewIds from meta so filters/sorts are respected
1982
+ const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
1983
+ const viewIds = meta?.arrayViews?.[arrayPathKey];
1984
+
1985
+ // Pass viewIds to getShadowValue to get the filtered/sorted data
1986
+ return getShadowValue(stateKey, path, viewIds);
1987
+ }
1988
+
1989
+ // Setter: state.count(5)
1990
+ const newValue = args[0];
1991
+ effectiveSetState(newValue, path, { updateType: 'update' });
1992
+ return true;
1993
+ },
1994
+
1995
+ get(target: any, prop: string, receiver: any) {
1996
+ if (prop === 'call' || prop === 'apply' || prop === 'bind') {
1997
+ return Reflect.get(target, prop, receiver);
1998
+ }
1999
+
1959
2000
  if (typeof prop !== 'string') {
1960
- // This is a Symbol. Let the default "get" behavior happen.
1961
- // This allows internal JS operations and dev tools to work correctly
1962
- // without interfering with your state logic.
1963
2001
  return Reflect.get(target, prop);
1964
2002
  }
1965
2003
  if (path.length === 0 && prop in rootLevelMethods) {
@@ -2044,7 +2082,7 @@ function createProxyHandler<T>(
2044
2082
  const getStatusFunc = () => {
2045
2083
  // ✅ Use the optimized helper to get all data in one efficient call
2046
2084
  const { shadowMeta, value } = getScopedData(stateKey, path, meta);
2047
- console.log('getStatusFunc', path, shadowMeta, value);
2085
+
2048
2086
  if (shadowMeta?.isDirty === true) {
2049
2087
  return 'dirty';
2050
2088
  }
@@ -2087,6 +2125,75 @@ function createProxyHandler<T>(
2087
2125
  if (storageKey) localStorage.removeItem(storageKey);
2088
2126
  };
2089
2127
  }
2128
+
2129
+ if (prop === '$validate') {
2130
+ return () => {
2131
+ const store = getGlobalStore.getState();
2132
+ // 1. Get current data and schema
2133
+ const { value } = getScopedData(stateKey, path, meta);
2134
+ const opts = store.getInitialOptions(stateKey);
2135
+ const schema =
2136
+ opts?.validation?.zodSchemaV4 || opts?.validation?.zodSchemaV3;
2137
+
2138
+ if (!schema) {
2139
+ return { success: true, data: value };
2140
+ }
2141
+
2142
+ // 2. Run Zod
2143
+ const result = (schema as any).safeParse(value);
2144
+
2145
+ // 3. Clear ANY existing errors for this path first (reset state)
2146
+ // You might want to be smarter about this for nested objects,
2147
+ // but effectively we need to wipe previous red borders before applying new ones.
2148
+ // (Using the logic from $clearZodValidation)
2149
+ const clearPath = (currentPath: string[]) => {
2150
+ const currentMeta =
2151
+ store.getShadowMetadata(stateKey, currentPath) || {};
2152
+ store.setShadowMetadata(stateKey, currentPath, {
2153
+ ...currentMeta,
2154
+ validation: {
2155
+ status: 'NOT_VALIDATED',
2156
+ errors: [],
2157
+ lastValidated: Date.now(),
2158
+ },
2159
+ });
2160
+ };
2161
+ // Note: Ideally you recursively clear errors here, but for now we proceed to add new ones.
2162
+
2163
+ // 4. If Invalid, apply errors to State (This turns the UI red)
2164
+ if (!result.success) {
2165
+ result.error.errors.forEach((error: any) => {
2166
+ // Calculate the exact path to the field with the error
2167
+ const errorPath = [...path, ...error.path.map(String)];
2168
+
2169
+ const currentMeta =
2170
+ store.getShadowMetadata(stateKey, errorPath) || {};
2171
+
2172
+ store.setShadowMetadata(stateKey, errorPath, {
2173
+ ...currentMeta,
2174
+ validation: {
2175
+ status: 'INVALID',
2176
+ errors: [
2177
+ {
2178
+ source: 'client',
2179
+ message: error.message,
2180
+ severity: 'error',
2181
+ code: error.code,
2182
+ },
2183
+ ],
2184
+ lastValidated: Date.now(),
2185
+ validatedValue: getShadowValue(stateKey, errorPath),
2186
+ },
2187
+ });
2188
+ });
2189
+
2190
+ // Notify components to re-render and show the errors
2191
+ notifyComponents(stateKey);
2192
+ }
2193
+
2194
+ return result;
2195
+ };
2196
+ }
2090
2197
  if (prop === '$showValidationErrors') {
2091
2198
  return () => {
2092
2199
  const { shadowMeta } = getScopedData(stateKey, path, meta);
@@ -2181,357 +2288,7 @@ function createProxyHandler<T>(
2181
2288
  };
2182
2289
  }
2183
2290
 
2184
- if (prop === '$useVirtualView') {
2185
- return (
2186
- options: VirtualViewOptions
2187
- ): VirtualStateObjectResult<any[]> => {
2188
- const {
2189
- itemHeight = 50,
2190
- overscan = 6,
2191
- stickToBottom = false,
2192
- scrollStickTolerance = 75,
2193
- } = options;
2194
-
2195
- const containerRef = useRef<HTMLDivElement | null>(null);
2196
- const [range, setRange] = useState({
2197
- startIndex: 0,
2198
- endIndex: 10,
2199
- });
2200
- const [rerender, forceUpdate] = useState({});
2201
- const initialScrollRef = useRef(true);
2202
-
2203
- useEffect(() => {
2204
- const interval = setInterval(() => {
2205
- forceUpdate({});
2206
- }, 1000);
2207
- return () => clearInterval(interval);
2208
- }, []);
2209
-
2210
- // Scroll state management
2211
- const scrollStateRef = useRef({
2212
- isUserScrolling: false,
2213
- lastScrollTop: 0,
2214
- scrollUpCount: 0,
2215
- isNearBottom: true,
2216
- });
2217
-
2218
- // Measurement cache
2219
- const measurementCache = useRef(
2220
- new Map<string, { height: number; offset: number }>()
2221
- );
2222
- const { keys: arrayKeys } = getArrayData(stateKey, path, meta);
2223
-
2224
- // Subscribe to state changes like stateList does
2225
- useEffect(() => {
2226
- const stateKeyPathKey = [stateKey, ...path].join('.');
2227
- const unsubscribe = getGlobalStore
2228
- .getState()
2229
- .subscribeToPath(stateKeyPathKey, (e) => {
2230
- if (e.type === 'GET_SELECTED') {
2231
- return;
2232
- }
2233
- if (e.type === 'SERVER_STATE_UPDATE') {
2234
- // forceUpdate({});
2235
- }
2236
- });
2237
-
2238
- return () => {
2239
- unsubscribe();
2240
- };
2241
- }, [componentId, stateKey, path.join('.')]);
2242
-
2243
- // YOUR ORIGINAL INITIAL POSITIONING - KEEPING EXACTLY AS IS
2244
- useLayoutEffect(() => {
2245
- if (
2246
- stickToBottom &&
2247
- arrayKeys.length > 0 &&
2248
- containerRef.current &&
2249
- !scrollStateRef.current.isUserScrolling &&
2250
- initialScrollRef.current
2251
- ) {
2252
- const container = containerRef.current;
2253
-
2254
- const waitForContainer = () => {
2255
- if (container.clientHeight > 0) {
2256
- const visibleCount = Math.ceil(
2257
- container.clientHeight / itemHeight
2258
- );
2259
- const endIndex = arrayKeys.length - 1;
2260
- const startIndex = Math.max(
2261
- 0,
2262
- endIndex - visibleCount - overscan
2263
- );
2264
-
2265
- setRange({ startIndex, endIndex });
2266
-
2267
- requestAnimationFrame(() => {
2268
- scrollToBottom('instant');
2269
- initialScrollRef.current = false;
2270
- });
2271
- } else {
2272
- requestAnimationFrame(waitForContainer);
2273
- }
2274
- };
2275
-
2276
- waitForContainer();
2277
- }
2278
- }, [arrayKeys.length, stickToBottom, itemHeight, overscan]);
2279
-
2280
- const rangeRef = useRef(range);
2281
- useLayoutEffect(() => {
2282
- rangeRef.current = range;
2283
- }, [range]);
2284
-
2285
- const arrayKeysRef = useRef(arrayKeys);
2286
- useLayoutEffect(() => {
2287
- arrayKeysRef.current = arrayKeys;
2288
- }, [arrayKeys]);
2289
-
2290
- const handleScroll = useCallback(() => {
2291
- const container = containerRef.current;
2292
- if (!container) return;
2293
-
2294
- const currentScrollTop = container.scrollTop;
2295
- const { scrollHeight, clientHeight } = container;
2296
- const scrollState = scrollStateRef.current;
2297
-
2298
- // Check if user is near bottom
2299
- const distanceFromBottom =
2300
- scrollHeight - (currentScrollTop + clientHeight);
2301
- const wasNearBottom = scrollState.isNearBottom;
2302
- scrollState.isNearBottom =
2303
- distanceFromBottom <= scrollStickTolerance;
2304
-
2305
- // Detect scroll direction
2306
- if (currentScrollTop < scrollState.lastScrollTop) {
2307
- // User scrolled up
2308
- scrollState.scrollUpCount++;
2309
-
2310
- if (scrollState.scrollUpCount > 3 && wasNearBottom) {
2311
- // User has deliberately scrolled away from bottom
2312
- scrollState.isUserScrolling = true;
2313
- console.log('User scrolled away from bottom');
2314
- }
2315
- } else if (scrollState.isNearBottom) {
2316
- // Reset if we're back near the bottom
2317
- scrollState.isUserScrolling = false;
2318
- scrollState.scrollUpCount = 0;
2319
- }
2320
-
2321
- scrollState.lastScrollTop = currentScrollTop;
2322
-
2323
- // Update visible range
2324
- let newStartIndex = 0;
2325
- for (let i = 0; i < arrayKeys.length; i++) {
2326
- const itemKey = arrayKeys[i];
2327
- const item = measurementCache.current.get(itemKey!);
2328
- if (item && item.offset + item.height > currentScrollTop) {
2329
- newStartIndex = i;
2330
- break;
2331
- }
2332
- }
2333
- console.log(
2334
- 'hadnlescroll ',
2335
- measurementCache.current,
2336
- newStartIndex,
2337
- range
2338
- );
2339
- // Only update if range actually changed
2340
- if (newStartIndex !== range.startIndex && range.startIndex != 0) {
2341
- const visibleCount = Math.ceil(clientHeight / itemHeight);
2342
- setRange({
2343
- startIndex: Math.max(0, newStartIndex - overscan),
2344
- endIndex: Math.min(
2345
- arrayKeys.length - 1,
2346
- newStartIndex + visibleCount + overscan
2347
- ),
2348
- });
2349
- }
2350
- }, [
2351
- arrayKeys.length,
2352
- range.startIndex,
2353
- itemHeight,
2354
- overscan,
2355
- scrollStickTolerance,
2356
- ]);
2357
-
2358
- // Set up scroll listener
2359
- useEffect(() => {
2360
- const container = containerRef.current;
2361
- if (!container) return;
2362
-
2363
- container.addEventListener('scroll', handleScroll, {
2364
- passive: true,
2365
- });
2366
- return () => {
2367
- container.removeEventListener('scroll', handleScroll);
2368
- };
2369
- }, [handleScroll, stickToBottom]);
2370
-
2371
- // YOUR ORIGINAL SCROLL TO BOTTOM FUNCTION - KEEPING EXACTLY AS IS
2372
- const scrollToBottom = useCallback(
2373
- (behavior: ScrollBehavior = 'smooth') => {
2374
- const container = containerRef.current;
2375
- if (!container) return;
2376
-
2377
- scrollStateRef.current.isUserScrolling = false;
2378
- scrollStateRef.current.isNearBottom = true;
2379
- scrollStateRef.current.scrollUpCount = 0;
2380
-
2381
- const performScroll = () => {
2382
- const attemptScroll = (attempts = 0) => {
2383
- if (attempts > 5) return;
2384
-
2385
- const currentHeight = container.scrollHeight;
2386
- const currentScroll = container.scrollTop;
2387
- const clientHeight = container.clientHeight;
2388
-
2389
- if (currentScroll + clientHeight >= currentHeight - 1) {
2390
- return;
2391
- }
2392
-
2393
- container.scrollTo({
2394
- top: currentHeight,
2395
- behavior: behavior,
2396
- });
2397
-
2398
- setTimeout(() => {
2399
- const newHeight = container.scrollHeight;
2400
- const newScroll = container.scrollTop;
2401
-
2402
- if (
2403
- newHeight !== currentHeight ||
2404
- newScroll + clientHeight < newHeight - 1
2405
- ) {
2406
- attemptScroll(attempts + 1);
2407
- }
2408
- }, 50);
2409
- };
2410
-
2411
- attemptScroll();
2412
- };
2413
-
2414
- if ('requestIdleCallback' in window) {
2415
- requestIdleCallback(performScroll, { timeout: 100 });
2416
- } else {
2417
- requestAnimationFrame(() => {
2418
- requestAnimationFrame(performScroll);
2419
- });
2420
- }
2421
- },
2422
- []
2423
- );
2424
-
2425
- // YOUR ORIGINAL AUTO-SCROLL EFFECTS - KEEPING ALL OF THEM
2426
- useEffect(() => {
2427
- if (!stickToBottom || !containerRef.current) return;
2428
-
2429
- const container = containerRef.current;
2430
- const scrollState = scrollStateRef.current;
2431
-
2432
- let scrollTimeout: NodeJS.Timeout;
2433
- const debouncedScrollToBottom = () => {
2434
- clearTimeout(scrollTimeout);
2435
- scrollTimeout = setTimeout(() => {
2436
- if (
2437
- !scrollState.isUserScrolling &&
2438
- scrollState.isNearBottom
2439
- ) {
2440
- scrollToBottom(
2441
- initialScrollRef.current ? 'instant' : 'smooth'
2442
- );
2443
- }
2444
- }, 100);
2445
- };
2446
-
2447
- const observer = new MutationObserver(() => {
2448
- if (!scrollState.isUserScrolling) {
2449
- debouncedScrollToBottom();
2450
- }
2451
- });
2452
-
2453
- observer.observe(container, {
2454
- childList: true,
2455
- subtree: true,
2456
- attributes: true,
2457
- attributeFilter: ['style', 'class'],
2458
- });
2459
-
2460
- if (initialScrollRef.current) {
2461
- setTimeout(() => {
2462
- scrollToBottom('instant');
2463
- }, 0);
2464
- } else {
2465
- debouncedScrollToBottom();
2466
- }
2467
-
2468
- return () => {
2469
- clearTimeout(scrollTimeout);
2470
- observer.disconnect();
2471
- };
2472
- }, [stickToBottom, arrayKeys.length, scrollToBottom]);
2473
-
2474
- // Create virtual state - NO NEED to get values, only IDs!
2475
- const virtualState = useMemo(() => {
2476
- // 2. Physically slice the corresponding keys.
2477
- const slicedKeys = Array.isArray(arrayKeys)
2478
- ? arrayKeys.slice(range.startIndex, range.endIndex + 1)
2479
- : [];
2480
-
2481
- // Use the same keying as getArrayData (empty string for root)
2482
- const arrayPath = path.length > 0 ? path.join('.') : 'root';
2483
- return rebuildStateShape({
2484
- path,
2485
- componentId: componentId!,
2486
- meta: {
2487
- ...meta,
2488
- arrayViews: { [arrayPath]: slicedKeys },
2489
- serverStateIsUpStream: true,
2490
- },
2491
- });
2492
- }, [range.startIndex, range.endIndex, arrayKeys, meta]);
2493
-
2494
- return {
2495
- virtualState,
2496
- virtualizerProps: {
2497
- outer: {
2498
- ref: containerRef,
2499
- style: {
2500
- overflowY: 'auto' as const,
2501
- height: '100%',
2502
- position: 'relative' as const,
2503
- },
2504
- },
2505
- inner: {
2506
- style: {
2507
- position: 'relative' as const,
2508
- },
2509
- },
2510
- list: {
2511
- style: {
2512
- transform: `translateY(${
2513
- measurementCache.current.get(arrayKeys[range.startIndex]!)
2514
- ?.offset || 0
2515
- }px)`,
2516
- },
2517
- },
2518
- },
2519
- scrollToBottom,
2520
- scrollToIndex: (
2521
- index: number,
2522
- behavior: ScrollBehavior = 'smooth'
2523
- ) => {
2524
- if (containerRef.current && arrayKeys[index]) {
2525
- const offset =
2526
- measurementCache.current.get(arrayKeys[index]!)?.offset ||
2527
- 0;
2528
- containerRef.current.scrollTo({ top: offset, behavior });
2529
- }
2530
- },
2531
- };
2532
- };
2533
- }
2534
- if (prop === '$stateMap') {
2291
+ if (prop === '$map') {
2535
2292
  return (
2536
2293
  callbackfn: (setter: any, index: number, arraySetter: any) => void
2537
2294
  ) => {
@@ -2571,7 +2328,7 @@ function createProxyHandler<T>(
2571
2328
  };
2572
2329
  }
2573
2330
 
2574
- if (prop === '$stateFilter') {
2331
+ if (prop === '$filter') {
2575
2332
  return (callbackfn: (value: any, index: number) => boolean) => {
2576
2333
  const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
2577
2334
 
@@ -2583,7 +2340,7 @@ function createProxyHandler<T>(
2583
2340
  );
2584
2341
 
2585
2342
  if (!Array.isArray(array)) {
2586
- throw new Error('stateFilter can only be used on arrays');
2343
+ throw new Error('filter can only be used on arrays');
2587
2344
  }
2588
2345
 
2589
2346
  // Filter the array and collect the IDs of the items that pass
@@ -2616,7 +2373,7 @@ function createProxyHandler<T>(
2616
2373
  });
2617
2374
  };
2618
2375
  }
2619
- if (prop === '$stateSort') {
2376
+ if (prop === '$sort') {
2620
2377
  return (compareFn: (a: any, b: any) => number) => {
2621
2378
  const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
2622
2379
 
@@ -2759,7 +2516,7 @@ function createProxyHandler<T>(
2759
2516
  };
2760
2517
  }
2761
2518
 
2762
- if (prop === '$stateList') {
2519
+ if (prop === '$list') {
2763
2520
  return (
2764
2521
  callbackfn: (
2765
2522
  setter: any,
@@ -2767,7 +2524,7 @@ function createProxyHandler<T>(
2767
2524
  arraySetter: any
2768
2525
  ) => ReactNode
2769
2526
  ) => {
2770
- const StateListWrapper = () => {
2527
+ const ListWrapper = () => {
2771
2528
  const componentIdsRef = useRef<Map<string, string>>(new Map());
2772
2529
 
2773
2530
  const [updateTrigger, forceUpdate] = useState({});
@@ -2877,7 +2634,7 @@ function createProxyHandler<T>(
2877
2634
  return <>{returnValue}</>;
2878
2635
  };
2879
2636
 
2880
- return <StateListWrapper />;
2637
+ return <ListWrapper />;
2881
2638
  };
2882
2639
  }
2883
2640
  if (prop === '$stateFlattenOn') {
@@ -3159,54 +2916,6 @@ function createProxyHandler<T>(
3159
2916
  });
3160
2917
  }
3161
2918
 
3162
- // if (prop === '$formInput') {
3163
- // const _getFormElement = (path: string[]): HTMLElement | null => {
3164
- // const metadata = getShadowMetadata(stateKey, path);
3165
- // if (metadata?.formRef?.current) {
3166
- // return metadata.formRef.current;
3167
- // }
3168
- // // This warning is helpful for debugging if a ref is missing.
3169
- // console.warn(
3170
- // `Form element ref not found for stateKey "${stateKey}" at path "${path.join('.')}"`
3171
- // );
3172
- // return null;
3173
- // };
3174
- // return {
3175
- // setDisabled: (isDisabled: boolean) => {
3176
- // const element = _getFormElement(path) as HTMLInputElement | null;
3177
- // if (element) {
3178
- // element.disabled = isDisabled;
3179
- // }
3180
- // },
3181
- // focus: () => {
3182
- // const element = _getFormElement(path);
3183
- // element?.focus();
3184
- // },
3185
- // blur: () => {
3186
- // const element = _getFormElement(path);
3187
- // element?.blur();
3188
- // },
3189
- // scrollIntoView: (options?: ScrollIntoViewOptions) => {
3190
- // const element = _getFormElement(path);
3191
- // element?.scrollIntoView(
3192
- // options ?? { behavior: 'smooth', block: 'center' }
3193
- // );
3194
- // },
3195
- // click: () => {
3196
- // const element = _getFormElement(path);
3197
- // element?.click();
3198
- // },
3199
- // selectText: () => {
3200
- // const element = _getFormElement(path) as
3201
- // | HTMLInputElement
3202
- // | HTMLTextAreaElement
3203
- // | null;
3204
- // if (element && typeof element.select === 'function') {
3205
- // element.select();
3206
- // }
3207
- // },
3208
- // };
3209
- // }
3210
2919
  if (prop === '$$get') {
3211
2920
  return () =>
3212
2921
  $cogsSignal({ _stateKey: stateKey, _path: path, _meta: meta });
@@ -3374,33 +3083,6 @@ function createProxyHandler<T>(
3374
3083
  },
3375
3084
  metaData?: Record<string, any>
3376
3085
  ) => {
3377
- // const validationErrorsFromServer = operation.validation || [];
3378
-
3379
- // if (!operation || !operation.path) {
3380
- // console.error(
3381
- // 'Invalid operation received by $applyOperation:',
3382
- // operation
3383
- // );
3384
- // return;
3385
- // }
3386
-
3387
- // const newErrors: ValidationError[] =
3388
- // validationErrorsFromServer.map((err) => ({
3389
- // source: 'sync_engine',
3390
- // message: err.message,
3391
- // severity: 'warning',
3392
- // code: err.code,
3393
- // }));
3394
- // console.log('newErrors', newErrors);
3395
- // getGlobalStore
3396
- // .getState()
3397
- // .setShadowMetadata(stateKey, operation.path, {
3398
- // validation: {
3399
- // status: newErrors.length > 0 ? 'INVALID' : 'VALID',
3400
- // errors: newErrors,
3401
- // lastValidated: Date.now(),
3402
- // },
3403
- // });
3404
3086
  console.log(
3405
3087
  'getGlobalStore',
3406
3088
  getGlobalStore
@@ -3626,12 +3308,13 @@ function createProxyHandler<T>(
3626
3308
  },
3627
3309
  };
3628
3310
 
3629
- const proxyInstance = new Proxy({}, handler);
3311
+ const proxyInstance = new Proxy(proxyTarget, handler);
3630
3312
  proxyCache.set(cacheKey, proxyInstance);
3631
3313
 
3632
3314
  return proxyInstance;
3633
3315
  }
3634
3316
 
3317
+ // ... (rest of the function: rootLevelMethods, returnShape, etc.)
3635
3318
  const rootLevelMethods = {
3636
3319
  $revertToInitialState: (obj?: { validationKey?: string }) => {
3637
3320
  const shadowMeta = getGlobalStore