cogsbox-state 0.5.435 → 0.5.436
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/dist/CogsState.d.ts +31 -25
- package/dist/CogsState.jsx +1237 -1141
- package/dist/CogsState.jsx.map +1 -1
- package/dist/CogsStateClient.jsx.map +1 -1
- package/dist/Functions.d.ts +1 -5
- package/dist/Functions.jsx +17 -48
- package/dist/Functions.jsx.map +1 -1
- package/dist/TRPCValidationLink.js.map +1 -1
- package/dist/store.d.ts +11 -0
- package/dist/store.js +245 -185
- package/dist/store.js.map +1 -1
- package/dist/utility.js.map +1 -1
- package/package.json +6 -5
- package/src/CogsState.tsx +417 -176
- package/src/Functions.tsx +8 -241
- package/src/TRPCValidationLink.ts +12 -12
- package/src/store.ts +195 -38
- package/dist/useValidateZodPath.d.ts +0 -34
- package/src/useValidateZodPath.ts +0 -231
package/src/CogsState.tsx
CHANGED
|
@@ -26,12 +26,18 @@ import { ValidationWrapper } from './Functions.js';
|
|
|
26
26
|
import { isDeepEqual, transformStateFunc } from './utility.js';
|
|
27
27
|
import superjson from 'superjson';
|
|
28
28
|
import { v4 as uuidv4 } from 'uuid';
|
|
29
|
-
import { z } from 'zod';
|
|
30
29
|
|
|
31
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
formRefStore,
|
|
32
|
+
getGlobalStore,
|
|
33
|
+
ValidationStatus,
|
|
34
|
+
type ComponentsType,
|
|
35
|
+
} from './store.js';
|
|
32
36
|
import { useCogsConfig } from './CogsStateClient.js';
|
|
33
37
|
import { applyPatch, compare, Operation } from 'fast-json-patch';
|
|
34
38
|
import { useInView } from 'react-intersection-observer';
|
|
39
|
+
import * as z3 from 'zod/v3';
|
|
40
|
+
import * as z4 from 'zod/v4';
|
|
35
41
|
|
|
36
42
|
type Prettify<T> = T extends any ? { [K in keyof T]: T[K] } : never;
|
|
37
43
|
|
|
@@ -217,12 +223,15 @@ export type FormOptsType = {
|
|
|
217
223
|
validation?: {
|
|
218
224
|
hideMessage?: boolean;
|
|
219
225
|
message?: string;
|
|
220
|
-
|
|
226
|
+
|
|
221
227
|
props?: GenericObject;
|
|
222
228
|
disable?: boolean;
|
|
223
229
|
};
|
|
224
230
|
|
|
225
231
|
debounceTime?: number;
|
|
232
|
+
sync?: {
|
|
233
|
+
allowInvalidValues?: boolean; // default: false
|
|
234
|
+
};
|
|
226
235
|
};
|
|
227
236
|
|
|
228
237
|
export type FormControl<T> = (obj: FormElementParams<T>) => JSX.Element;
|
|
@@ -248,6 +257,7 @@ export type EndType<T, IsArrayElement = false> = {
|
|
|
248
257
|
_stateKey: string;
|
|
249
258
|
formElement: (control: FormControl<T>, opts?: FormOptsType) => JSX.Element;
|
|
250
259
|
get: () => T;
|
|
260
|
+
getState: () => T;
|
|
251
261
|
$get: () => T;
|
|
252
262
|
$derive: <R>(fn: EffectFunction<T, R>) => R;
|
|
253
263
|
|
|
@@ -317,13 +327,17 @@ type EffectiveSetStateArg<
|
|
|
317
327
|
? InsertParams<InferArrayElement<T>>
|
|
318
328
|
: never
|
|
319
329
|
: UpdateArg<T>;
|
|
330
|
+
type UpdateOptions = {
|
|
331
|
+
updateType: 'insert' | 'cut' | 'update';
|
|
320
332
|
|
|
333
|
+
sync?: boolean;
|
|
334
|
+
};
|
|
321
335
|
type EffectiveSetState<TStateObject> = (
|
|
322
336
|
newStateOrFunction:
|
|
323
337
|
| EffectiveSetStateArg<TStateObject, 'update'>
|
|
324
338
|
| EffectiveSetStateArg<TStateObject, 'insert'>,
|
|
325
339
|
path: string[],
|
|
326
|
-
updateObj:
|
|
340
|
+
updateObj: UpdateOptions,
|
|
327
341
|
validationKey?: string
|
|
328
342
|
) => void;
|
|
329
343
|
|
|
@@ -346,11 +360,6 @@ export type ReactivityType =
|
|
|
346
360
|
| 'all'
|
|
347
361
|
| Array<Prettify<'none' | 'component' | 'deps' | 'all'>>;
|
|
348
362
|
|
|
349
|
-
type ValidationOptionsType = {
|
|
350
|
-
key?: string;
|
|
351
|
-
zodSchema?: z.ZodTypeAny;
|
|
352
|
-
onBlur?: boolean;
|
|
353
|
-
};
|
|
354
363
|
// Define the return type of the sync hook locally
|
|
355
364
|
type SyncApi = {
|
|
356
365
|
updateState: (data: { operation: any }) => void;
|
|
@@ -358,6 +367,13 @@ type SyncApi = {
|
|
|
358
367
|
clientId: string | null;
|
|
359
368
|
subscribers: string[];
|
|
360
369
|
};
|
|
370
|
+
type ValidationOptionsType = {
|
|
371
|
+
key?: string;
|
|
372
|
+
zodSchemaV3?: z3.ZodType<any, any, any>;
|
|
373
|
+
zodSchemaV4?: z4.ZodType<any, any, any>;
|
|
374
|
+
|
|
375
|
+
onBlur?: boolean;
|
|
376
|
+
};
|
|
361
377
|
export type OptionsType<T extends unknown = unknown> = {
|
|
362
378
|
log?: boolean;
|
|
363
379
|
componentId?: string;
|
|
@@ -402,7 +418,7 @@ export type OptionsType<T extends unknown = unknown> = {
|
|
|
402
418
|
key: string | ((state: T) => string);
|
|
403
419
|
onChange?: (state: T) => void;
|
|
404
420
|
};
|
|
405
|
-
formElements?: FormsElementsType
|
|
421
|
+
formElements?: FormsElementsType<T>;
|
|
406
422
|
|
|
407
423
|
reactiveDeps?: (state: T) => any[] | true;
|
|
408
424
|
reactiveType?: ReactivityType;
|
|
@@ -412,15 +428,6 @@ export type OptionsType<T extends unknown = unknown> = {
|
|
|
412
428
|
dependencies?: any[];
|
|
413
429
|
};
|
|
414
430
|
|
|
415
|
-
export type ValidationWrapperOptions<T extends unknown = unknown> = {
|
|
416
|
-
children: React.ReactNode;
|
|
417
|
-
active: boolean;
|
|
418
|
-
stretch?: boolean;
|
|
419
|
-
path: string[];
|
|
420
|
-
message?: string;
|
|
421
|
-
data?: T;
|
|
422
|
-
key?: string;
|
|
423
|
-
};
|
|
424
431
|
export type SyncRenderOptions<T extends unknown = unknown> = {
|
|
425
432
|
children: React.ReactNode;
|
|
426
433
|
time: number;
|
|
@@ -428,8 +435,16 @@ export type SyncRenderOptions<T extends unknown = unknown> = {
|
|
|
428
435
|
key?: string;
|
|
429
436
|
};
|
|
430
437
|
|
|
431
|
-
type FormsElementsType<T
|
|
432
|
-
validation?: (options:
|
|
438
|
+
type FormsElementsType<T> = {
|
|
439
|
+
validation?: (options: {
|
|
440
|
+
children: React.ReactNode;
|
|
441
|
+
status: ValidationStatus; // Instead of 'active' boolean
|
|
442
|
+
|
|
443
|
+
path: string[];
|
|
444
|
+
message?: string;
|
|
445
|
+
data?: T;
|
|
446
|
+
key?: string;
|
|
447
|
+
}) => React.ReactNode;
|
|
433
448
|
syncRender?: (options: SyncRenderOptions<T>) => React.ReactNode;
|
|
434
449
|
};
|
|
435
450
|
|
|
@@ -518,9 +533,32 @@ export function addStateOptions<T extends unknown>(
|
|
|
518
533
|
) {
|
|
519
534
|
return { initialState: initialState, formElements, validation } as T;
|
|
520
535
|
}
|
|
536
|
+
type UseCogsStateHook<T extends Record<string, any>> = <
|
|
537
|
+
StateKey extends keyof TransformedStateType<T>,
|
|
538
|
+
>(
|
|
539
|
+
stateKey: StateKey,
|
|
540
|
+
options?: Prettify<OptionsType<TransformedStateType<T>[StateKey]>>
|
|
541
|
+
) => StateObject<TransformedStateType<T>[StateKey]>;
|
|
542
|
+
|
|
543
|
+
// Define the type for the options setter using the Transformed state
|
|
544
|
+
type SetCogsOptionsFunc<T extends Record<string, any>> = <
|
|
545
|
+
StateKey extends keyof TransformedStateType<T>,
|
|
546
|
+
>(
|
|
547
|
+
stateKey: StateKey,
|
|
548
|
+
options: OptionsType<TransformedStateType<T>[StateKey]>
|
|
549
|
+
) => void;
|
|
550
|
+
|
|
551
|
+
// Define the final API object shape
|
|
552
|
+
type CogsApi<T extends Record<string, any>> = {
|
|
553
|
+
useCogsState: UseCogsStateHook<T>;
|
|
554
|
+
setCogsOptions: SetCogsOptionsFunc<T>;
|
|
555
|
+
};
|
|
521
556
|
export const createCogsState = <State extends Record<StateKeys, unknown>>(
|
|
522
557
|
initialState: State,
|
|
523
|
-
opt?: {
|
|
558
|
+
opt?: {
|
|
559
|
+
formElements?: FormsElementsType<State>;
|
|
560
|
+
validation?: ValidationOptionsType;
|
|
561
|
+
}
|
|
524
562
|
) => {
|
|
525
563
|
let newInitialState = initialState;
|
|
526
564
|
|
|
@@ -575,12 +613,12 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
|
|
|
575
613
|
|
|
576
614
|
const useCogsState = <StateKey extends StateKeys>(
|
|
577
615
|
stateKey: StateKey,
|
|
578
|
-
options?: OptionsType<(typeof statePart)[StateKey]
|
|
616
|
+
options?: Prettify<OptionsType<(typeof statePart)[StateKey]>>
|
|
579
617
|
) => {
|
|
580
618
|
const [componentId] = useState(options?.componentId ?? uuidv4());
|
|
581
619
|
setOptions({
|
|
582
620
|
stateKey,
|
|
583
|
-
options,
|
|
621
|
+
options: options as any,
|
|
584
622
|
initialOptionsPart,
|
|
585
623
|
});
|
|
586
624
|
|
|
@@ -621,7 +659,7 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
|
|
|
621
659
|
notifyComponents(stateKey as string);
|
|
622
660
|
}
|
|
623
661
|
|
|
624
|
-
return { useCogsState, setCogsOptions }
|
|
662
|
+
return { useCogsState, setCogsOptions } as CogsApi<State>;
|
|
625
663
|
};
|
|
626
664
|
|
|
627
665
|
const {
|
|
@@ -1241,9 +1279,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1241
1279
|
...rootMeta,
|
|
1242
1280
|
components,
|
|
1243
1281
|
});
|
|
1244
|
-
|
|
1245
1282
|
forceUpdate({});
|
|
1246
|
-
|
|
1247
1283
|
return () => {
|
|
1248
1284
|
const meta = getGlobalStore.getState().getShadowMetadata(thisKey, []);
|
|
1249
1285
|
const component = meta?.components?.get(componentKey);
|
|
@@ -1282,8 +1318,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1282
1318
|
const effectiveSetState = (
|
|
1283
1319
|
newStateOrFunction: UpdateArg<TStateObject> | InsertParams<TStateObject>,
|
|
1284
1320
|
path: string[],
|
|
1285
|
-
updateObj:
|
|
1286
|
-
validationKey?: string
|
|
1321
|
+
updateObj: UpdateOptions
|
|
1287
1322
|
) => {
|
|
1288
1323
|
const fullPath = [thisKey, ...path].join('.');
|
|
1289
1324
|
if (Array.isArray(path)) {
|
|
@@ -1349,9 +1384,9 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1349
1384
|
break;
|
|
1350
1385
|
}
|
|
1351
1386
|
}
|
|
1387
|
+
const shouldSync = updateObj.sync !== false;
|
|
1352
1388
|
|
|
1353
|
-
|
|
1354
|
-
if (syncApiRef.current && syncApiRef.current.connected) {
|
|
1389
|
+
if (shouldSync && syncApiRef.current && syncApiRef.current.connected) {
|
|
1355
1390
|
syncApiRef.current.updateState({ operation: newUpdate });
|
|
1356
1391
|
}
|
|
1357
1392
|
// Handle signals - reuse shadowMeta from the beginning
|
|
@@ -1519,59 +1554,18 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1519
1554
|
});
|
|
1520
1555
|
}
|
|
1521
1556
|
}
|
|
1522
|
-
if (
|
|
1523
|
-
updateObj.updateType === 'update' &&
|
|
1524
|
-
(validationKey || latestInitialOptionsRef.current?.validation?.key) &&
|
|
1525
|
-
path
|
|
1526
|
-
) {
|
|
1527
|
-
removeValidationError(
|
|
1528
|
-
(validationKey || latestInitialOptionsRef.current?.validation?.key) +
|
|
1529
|
-
'.' +
|
|
1530
|
-
path.join('.')
|
|
1531
|
-
);
|
|
1532
|
-
}
|
|
1533
|
-
const arrayWithoutIndex = path.slice(0, path.length - 1);
|
|
1534
|
-
if (
|
|
1535
|
-
updateObj.updateType === 'cut' &&
|
|
1536
|
-
latestInitialOptionsRef.current?.validation?.key
|
|
1537
|
-
) {
|
|
1538
|
-
removeValidationError(
|
|
1539
|
-
latestInitialOptionsRef.current?.validation?.key +
|
|
1540
|
-
'.' +
|
|
1541
|
-
arrayWithoutIndex.join('.')
|
|
1542
|
-
);
|
|
1543
|
-
}
|
|
1544
|
-
if (
|
|
1545
|
-
updateObj.updateType === 'insert' &&
|
|
1546
|
-
latestInitialOptionsRef.current?.validation?.key
|
|
1547
|
-
) {
|
|
1548
|
-
const getValidation = getValidationErrors(
|
|
1549
|
-
latestInitialOptionsRef.current?.validation?.key +
|
|
1550
|
-
'.' +
|
|
1551
|
-
arrayWithoutIndex.join('.')
|
|
1552
|
-
);
|
|
1553
|
-
|
|
1554
|
-
getValidation.filter((k) => {
|
|
1555
|
-
let length = k?.split('.').length;
|
|
1556
|
-
const v = ''; // Placeholder as `v` is not used from getValidationErrors
|
|
1557
1557
|
|
|
1558
|
-
if (
|
|
1559
|
-
k == arrayWithoutIndex.join('.') &&
|
|
1560
|
-
length == arrayWithoutIndex.length - 1
|
|
1561
|
-
) {
|
|
1562
|
-
let newKey = k + '.' + arrayWithoutIndex;
|
|
1563
|
-
removeValidationError(k!);
|
|
1564
|
-
addValidationError(newKey, v!);
|
|
1565
|
-
}
|
|
1566
|
-
});
|
|
1567
|
-
}
|
|
1568
1558
|
// Assumes `isDeepEqual` is available in this scope.
|
|
1569
1559
|
// Assumes `isDeepEqual` is available in this scope.
|
|
1570
1560
|
|
|
1571
|
-
const newState =
|
|
1572
|
-
const rootMeta =
|
|
1561
|
+
const newState = getGlobalStore.getState().getShadowValue(thisKey);
|
|
1562
|
+
const rootMeta = getGlobalStore.getState().getShadowMetadata(thisKey, []);
|
|
1573
1563
|
const notifiedComponents = new Set<string>();
|
|
1574
|
-
|
|
1564
|
+
console.log(
|
|
1565
|
+
'rootMeta',
|
|
1566
|
+
thisKey,
|
|
1567
|
+
getGlobalStore.getState().shadowStateStore
|
|
1568
|
+
);
|
|
1575
1569
|
if (!rootMeta?.components) {
|
|
1576
1570
|
return newState;
|
|
1577
1571
|
}
|
|
@@ -1579,28 +1573,39 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1579
1573
|
// --- PASS 1: Notify specific subscribers based on update type ---
|
|
1580
1574
|
|
|
1581
1575
|
if (updateObj.updateType === 'update') {
|
|
1582
|
-
//
|
|
1583
|
-
//
|
|
1584
|
-
//
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1576
|
+
// --- Bubble-up Notification ---
|
|
1577
|
+
// When a nested property changes, notify components listening at that exact path,
|
|
1578
|
+
// and also "bubble up" to notify components listening on parent paths.
|
|
1579
|
+
// e.g., an update to `user.address.street` notifies listeners of `street`, `address`, and `user`.
|
|
1580
|
+
let currentPath = [...path]; // Create a mutable copy of the path
|
|
1581
|
+
|
|
1582
|
+
while (true) {
|
|
1583
|
+
const currentPathMeta = store.getShadowMetadata(thisKey, currentPath);
|
|
1584
|
+
|
|
1585
|
+
if (currentPathMeta?.pathComponents) {
|
|
1586
|
+
currentPathMeta.pathComponents.forEach((componentId) => {
|
|
1587
|
+
if (notifiedComponents.has(componentId)) {
|
|
1588
|
+
return; // Avoid sending redundant notifications
|
|
1589
|
+
}
|
|
1590
|
+
const component = rootMeta.components?.get(componentId);
|
|
1591
|
+
if (component) {
|
|
1592
|
+
const reactiveTypes = Array.isArray(component.reactiveType)
|
|
1593
|
+
? component.reactiveType
|
|
1594
|
+
: [component.reactiveType || 'component'];
|
|
1596
1595
|
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1596
|
+
// This notification logic applies to components that depend on object structures.
|
|
1597
|
+
if (!reactiveTypes.includes('none')) {
|
|
1598
|
+
component.forceUpdate();
|
|
1599
|
+
notifiedComponents.add(componentId);
|
|
1600
|
+
}
|
|
1601
1601
|
}
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
if (currentPath.length === 0) {
|
|
1606
|
+
break; // We've reached the root, stop bubbling.
|
|
1607
|
+
}
|
|
1608
|
+
currentPath.pop(); // Go up one level for the next iteration.
|
|
1604
1609
|
}
|
|
1605
1610
|
|
|
1606
1611
|
// ADDITIONALLY, if the payload is an object, perform a deep-check and
|
|
@@ -1880,12 +1885,16 @@ const registerComponentDependency = (
|
|
|
1880
1885
|
dependencyPath: string[]
|
|
1881
1886
|
) => {
|
|
1882
1887
|
const fullComponentId = `${stateKey}////${componentId}`;
|
|
1883
|
-
const
|
|
1888
|
+
const { addPathComponent, getShadowMetadata } = getGlobalStore.getState();
|
|
1889
|
+
|
|
1890
|
+
// First, check if the component should even be registered.
|
|
1891
|
+
// This check is safe to do outside the setter.
|
|
1892
|
+
const rootMeta = getShadowMetadata(stateKey, []);
|
|
1884
1893
|
const component = rootMeta?.components?.get(fullComponentId);
|
|
1885
1894
|
|
|
1886
1895
|
if (
|
|
1887
1896
|
!component ||
|
|
1888
|
-
component.reactiveType
|
|
1897
|
+
component.reactiveType === 'none' ||
|
|
1889
1898
|
!(
|
|
1890
1899
|
Array.isArray(component.reactiveType)
|
|
1891
1900
|
? component.reactiveType
|
|
@@ -1895,23 +1904,9 @@ const registerComponentDependency = (
|
|
|
1895
1904
|
return;
|
|
1896
1905
|
}
|
|
1897
1906
|
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
// Add to component's paths (existing logic)
|
|
1901
|
-
component.paths.add(pathKey);
|
|
1902
|
-
|
|
1903
|
-
// NEW: Also store componentId at the path level
|
|
1904
|
-
const pathMeta =
|
|
1905
|
-
getGlobalStore.getState().getShadowMetadata(stateKey, dependencyPath) || {};
|
|
1906
|
-
const pathComponents = pathMeta.pathComponents || new Set<string>();
|
|
1907
|
-
pathComponents.add(fullComponentId);
|
|
1908
|
-
|
|
1909
|
-
getGlobalStore.getState().setShadowMetadata(stateKey, dependencyPath, {
|
|
1910
|
-
...pathMeta,
|
|
1911
|
-
pathComponents,
|
|
1912
|
-
});
|
|
1907
|
+
// Now, call the single, safe, atomic function to perform the update.
|
|
1908
|
+
addPathComponent(stateKey, dependencyPath, fullComponentId);
|
|
1913
1909
|
};
|
|
1914
|
-
|
|
1915
1910
|
const notifySelectionComponents = (
|
|
1916
1911
|
stateKey: string,
|
|
1917
1912
|
parentPath: string[],
|
|
@@ -2184,13 +2179,16 @@ function createProxyHandler<T>(
|
|
|
2184
2179
|
}
|
|
2185
2180
|
if (prop === 'showValidationErrors') {
|
|
2186
2181
|
return () => {
|
|
2187
|
-
const
|
|
2188
|
-
.getState()
|
|
2189
|
-
.getInitialOptions(stateKey)?.validation;
|
|
2190
|
-
if (!init?.key) throw new Error('Validation key not found');
|
|
2191
|
-
return getGlobalStore
|
|
2182
|
+
const meta = getGlobalStore
|
|
2192
2183
|
.getState()
|
|
2193
|
-
.
|
|
2184
|
+
.getShadowMetadata(stateKey, path);
|
|
2185
|
+
if (
|
|
2186
|
+
meta?.validation?.status === 'VALIDATION_FAILED' &&
|
|
2187
|
+
meta.validation.message
|
|
2188
|
+
) {
|
|
2189
|
+
return [meta.validation.message];
|
|
2190
|
+
}
|
|
2191
|
+
return [];
|
|
2194
2192
|
};
|
|
2195
2193
|
}
|
|
2196
2194
|
if (Array.isArray(currentState)) {
|
|
@@ -3019,7 +3017,6 @@ function createProxyHandler<T>(
|
|
|
3019
3017
|
e.type === 'REMOVE' ||
|
|
3020
3018
|
e.type === 'CLEAR_SELECTION'
|
|
3021
3019
|
) {
|
|
3022
|
-
console.log('sssssssssssssssssssssssssssss', e);
|
|
3023
3020
|
forceUpdate({});
|
|
3024
3021
|
}
|
|
3025
3022
|
});
|
|
@@ -3221,15 +3218,12 @@ function createProxyHandler<T>(
|
|
|
3221
3218
|
}
|
|
3222
3219
|
if (prop === 'cutSelected') {
|
|
3223
3220
|
return () => {
|
|
3224
|
-
const baseArrayKeys =
|
|
3225
|
-
getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
3226
|
-
?.arrayKeys || [];
|
|
3227
3221
|
const validKeys = applyTransforms(
|
|
3228
3222
|
stateKey,
|
|
3229
3223
|
path,
|
|
3230
3224
|
meta?.transforms
|
|
3231
3225
|
);
|
|
3232
|
-
|
|
3226
|
+
|
|
3233
3227
|
if (!validKeys || validKeys.length === 0) return;
|
|
3234
3228
|
|
|
3235
3229
|
const indexKeyToCut = getGlobalStore
|
|
@@ -3239,13 +3233,15 @@ function createProxyHandler<T>(
|
|
|
3239
3233
|
let indexToCut = validKeys.findIndex(
|
|
3240
3234
|
(key) => key === indexKeyToCut
|
|
3241
3235
|
);
|
|
3242
|
-
|
|
3236
|
+
|
|
3243
3237
|
const pathForCut = validKeys[
|
|
3244
3238
|
indexToCut == -1 ? validKeys.length - 1 : indexToCut
|
|
3245
3239
|
]
|
|
3246
3240
|
?.split('.')
|
|
3247
3241
|
.slice(1);
|
|
3248
|
-
|
|
3242
|
+
getGlobalStore
|
|
3243
|
+
.getState()
|
|
3244
|
+
.clearSelectedIndex({ arrayKey: stateKeyPathKey });
|
|
3249
3245
|
effectiveSetState(currentState, pathForCut!, {
|
|
3250
3246
|
updateType: 'cut',
|
|
3251
3247
|
});
|
|
@@ -3371,6 +3367,14 @@ function createProxyHandler<T>(
|
|
|
3371
3367
|
.getShadowValue(stateKeyPathKey, meta?.validIds);
|
|
3372
3368
|
};
|
|
3373
3369
|
}
|
|
3370
|
+
if (prop === 'getState') {
|
|
3371
|
+
return () => {
|
|
3372
|
+
return getGlobalStore
|
|
3373
|
+
.getState()
|
|
3374
|
+
.getShadowValue(stateKeyPathKey, meta?.validIds);
|
|
3375
|
+
};
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3374
3378
|
if (prop === '$derive') {
|
|
3375
3379
|
return (fn: any) =>
|
|
3376
3380
|
$cogsSignal({
|
|
@@ -3481,8 +3485,9 @@ function createProxyHandler<T>(
|
|
|
3481
3485
|
}
|
|
3482
3486
|
if (prop === 'applyJsonPatch') {
|
|
3483
3487
|
return (patches: Operation[]) => {
|
|
3484
|
-
// 1. Get the current state object that the proxy points to.
|
|
3485
3488
|
const store = getGlobalStore.getState();
|
|
3489
|
+
const rootMeta = store.getShadowMetadata(stateKey, []);
|
|
3490
|
+
if (!rootMeta?.components) return;
|
|
3486
3491
|
|
|
3487
3492
|
const convertPath = (jsonPath: string): string[] => {
|
|
3488
3493
|
if (!jsonPath || jsonPath === '/') return [];
|
|
@@ -3492,6 +3497,8 @@ function createProxyHandler<T>(
|
|
|
3492
3497
|
.map((p) => p.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
3493
3498
|
};
|
|
3494
3499
|
|
|
3500
|
+
const notifiedComponents = new Set<string>();
|
|
3501
|
+
|
|
3495
3502
|
for (const patch of patches) {
|
|
3496
3503
|
const relativePath = convertPath(patch.path);
|
|
3497
3504
|
|
|
@@ -3504,18 +3511,64 @@ function createProxyHandler<T>(
|
|
|
3504
3511
|
value: any;
|
|
3505
3512
|
};
|
|
3506
3513
|
store.updateShadowAtPath(stateKey, relativePath, value);
|
|
3507
|
-
store.markAsDirty(stateKey, relativePath, { bubble: true });
|
|
3514
|
+
store.markAsDirty(stateKey, relativePath, { bubble: true });
|
|
3515
|
+
|
|
3516
|
+
// Bubble up - notify components at this path and all parent paths
|
|
3517
|
+
let currentPath = [...relativePath];
|
|
3518
|
+
while (true) {
|
|
3519
|
+
const pathMeta = store.getShadowMetadata(
|
|
3520
|
+
stateKey,
|
|
3521
|
+
currentPath
|
|
3522
|
+
);
|
|
3523
|
+
console.log('pathMeta', pathMeta);
|
|
3524
|
+
if (pathMeta?.pathComponents) {
|
|
3525
|
+
pathMeta.pathComponents.forEach((componentId) => {
|
|
3526
|
+
if (!notifiedComponents.has(componentId)) {
|
|
3527
|
+
const component =
|
|
3528
|
+
rootMeta.components?.get(componentId);
|
|
3529
|
+
if (component) {
|
|
3530
|
+
component.forceUpdate();
|
|
3531
|
+
notifiedComponents.add(componentId);
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
});
|
|
3535
|
+
}
|
|
3536
|
+
|
|
3537
|
+
if (currentPath.length === 0) break;
|
|
3538
|
+
currentPath.pop(); // Go up one level
|
|
3539
|
+
}
|
|
3508
3540
|
break;
|
|
3509
3541
|
}
|
|
3510
3542
|
case 'remove': {
|
|
3511
|
-
store.removeShadowArrayElement(stateKey, relativePath);
|
|
3512
3543
|
const parentPath = relativePath.slice(0, -1);
|
|
3544
|
+
store.removeShadowArrayElement(stateKey, relativePath);
|
|
3513
3545
|
store.markAsDirty(stateKey, parentPath, { bubble: true });
|
|
3546
|
+
|
|
3547
|
+
// Bubble up from parent path
|
|
3548
|
+
let currentPath = [...parentPath];
|
|
3549
|
+
while (true) {
|
|
3550
|
+
const pathMeta = store.getShadowMetadata(
|
|
3551
|
+
stateKey,
|
|
3552
|
+
currentPath
|
|
3553
|
+
);
|
|
3554
|
+
if (pathMeta?.pathComponents) {
|
|
3555
|
+
pathMeta.pathComponents.forEach((componentId) => {
|
|
3556
|
+
if (!notifiedComponents.has(componentId)) {
|
|
3557
|
+
const component =
|
|
3558
|
+
rootMeta.components?.get(componentId);
|
|
3559
|
+
if (component) {
|
|
3560
|
+
component.forceUpdate();
|
|
3561
|
+
notifiedComponents.add(componentId);
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
});
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3567
|
+
if (currentPath.length === 0) break;
|
|
3568
|
+
currentPath.pop();
|
|
3569
|
+
}
|
|
3514
3570
|
break;
|
|
3515
3571
|
}
|
|
3516
|
-
// NOTE: 'move' and 'copy' operations should be deconstructed into 'remove' and 'add'
|
|
3517
|
-
// by the server before broadcasting for maximum compatibility. Your server's use
|
|
3518
|
-
// of `compare()` already does this, so we don't need to handle them here.
|
|
3519
3572
|
}
|
|
3520
3573
|
}
|
|
3521
3574
|
};
|
|
@@ -3525,20 +3578,40 @@ function createProxyHandler<T>(
|
|
|
3525
3578
|
const init = getGlobalStore
|
|
3526
3579
|
.getState()
|
|
3527
3580
|
.getInitialOptions(stateKey)?.validation;
|
|
3528
|
-
|
|
3529
|
-
|
|
3581
|
+
|
|
3582
|
+
// UPDATED: Select v4 schema, with a fallback to v3
|
|
3583
|
+
const zodSchema = init?.zodSchemaV4 || init?.zodSchemaV3;
|
|
3584
|
+
|
|
3585
|
+
if (!zodSchema || !init?.key) {
|
|
3586
|
+
throw new Error(
|
|
3587
|
+
'Zod schema (v3 or v4) or validation key not found'
|
|
3588
|
+
);
|
|
3589
|
+
}
|
|
3530
3590
|
|
|
3531
3591
|
removeValidationError(init.key);
|
|
3532
3592
|
const thisObject = getGlobalStore
|
|
3533
3593
|
.getState()
|
|
3534
3594
|
.getShadowValue(stateKey);
|
|
3535
|
-
|
|
3595
|
+
|
|
3596
|
+
// Use the selected schema for parsing
|
|
3597
|
+
const result = zodSchema.safeParse(thisObject);
|
|
3536
3598
|
|
|
3537
3599
|
if (!result.success) {
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3600
|
+
// This logic already handles both v3 and v4 error types correctly
|
|
3601
|
+
if ('issues' in result.error) {
|
|
3602
|
+
// Zod v4 error
|
|
3603
|
+
result.error.issues.forEach((error) => {
|
|
3604
|
+
const fullErrorPath = [init.key, ...error.path].join('.');
|
|
3605
|
+
addValidationError(fullErrorPath, error.message);
|
|
3606
|
+
});
|
|
3607
|
+
} else {
|
|
3608
|
+
// Zod v3 error
|
|
3609
|
+
(result.error as any).errors.forEach((error: any) => {
|
|
3610
|
+
const fullErrorPath = [init.key, ...error.path].join('.');
|
|
3611
|
+
addValidationError(fullErrorPath, error.message);
|
|
3612
|
+
});
|
|
3613
|
+
}
|
|
3614
|
+
|
|
3542
3615
|
notifyComponents(stateKey);
|
|
3543
3616
|
return false;
|
|
3544
3617
|
}
|
|
@@ -3631,20 +3704,14 @@ function createProxyHandler<T>(
|
|
|
3631
3704
|
if (prop === 'formElement') {
|
|
3632
3705
|
return (child: FormControl<T>, formOpts?: FormOptsType) => {
|
|
3633
3706
|
return (
|
|
3634
|
-
<
|
|
3635
|
-
formOpts={formOpts}
|
|
3636
|
-
path={path}
|
|
3707
|
+
<FormElementWrapper
|
|
3637
3708
|
stateKey={stateKey}
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
formOpts={formOpts}
|
|
3645
|
-
renderFn={child as any}
|
|
3646
|
-
/>
|
|
3647
|
-
</ValidationWrapper>
|
|
3709
|
+
path={path}
|
|
3710
|
+
rebuildStateShape={rebuildStateShape}
|
|
3711
|
+
setState={effectiveSetState}
|
|
3712
|
+
formOpts={formOpts}
|
|
3713
|
+
renderFn={child as any}
|
|
3714
|
+
/>
|
|
3648
3715
|
);
|
|
3649
3716
|
};
|
|
3650
3717
|
}
|
|
@@ -4203,6 +4270,7 @@ function ListItemWrapper({
|
|
|
4203
4270
|
|
|
4204
4271
|
return <div ref={setRefs}>{children}</div>;
|
|
4205
4272
|
}
|
|
4273
|
+
|
|
4206
4274
|
function FormElementWrapper({
|
|
4207
4275
|
stateKey,
|
|
4208
4276
|
path,
|
|
@@ -4248,7 +4316,9 @@ function FormElementWrapper({
|
|
|
4248
4316
|
const unsubscribe = getGlobalStore
|
|
4249
4317
|
.getState()
|
|
4250
4318
|
.subscribeToPath(stateKeyPathKey, (newValue) => {
|
|
4251
|
-
|
|
4319
|
+
if (!isCurrentlyDebouncing.current && localValue !== newValue) {
|
|
4320
|
+
forceUpdate({});
|
|
4321
|
+
}
|
|
4252
4322
|
});
|
|
4253
4323
|
return () => {
|
|
4254
4324
|
unsubscribe();
|
|
@@ -4261,6 +4331,10 @@ function FormElementWrapper({
|
|
|
4261
4331
|
|
|
4262
4332
|
const debouncedUpdate = useCallback(
|
|
4263
4333
|
(newValue: any) => {
|
|
4334
|
+
const currentType = typeof globalStateValue;
|
|
4335
|
+
if (currentType === 'number' && typeof newValue === 'string') {
|
|
4336
|
+
newValue = newValue === '' ? 0 : Number(newValue);
|
|
4337
|
+
}
|
|
4264
4338
|
setLocalValue(newValue);
|
|
4265
4339
|
isCurrentlyDebouncing.current = true;
|
|
4266
4340
|
|
|
@@ -4272,19 +4346,188 @@ function FormElementWrapper({
|
|
|
4272
4346
|
|
|
4273
4347
|
debounceTimeoutRef.current = setTimeout(() => {
|
|
4274
4348
|
isCurrentlyDebouncing.current = false;
|
|
4349
|
+
|
|
4350
|
+
// Update state
|
|
4275
4351
|
setState(newValue, path, { updateType: 'update' });
|
|
4352
|
+
|
|
4353
|
+
// Perform LIVE validation (gentle)
|
|
4354
|
+
const { getInitialOptions, setShadowMetadata, getShadowMetadata } =
|
|
4355
|
+
getGlobalStore.getState();
|
|
4356
|
+
const validationOptions = getInitialOptions(stateKey)?.validation;
|
|
4357
|
+
const zodSchema =
|
|
4358
|
+
validationOptions?.zodSchemaV4 || validationOptions?.zodSchemaV3;
|
|
4359
|
+
|
|
4360
|
+
if (zodSchema) {
|
|
4361
|
+
const fullState = getGlobalStore.getState().getShadowValue(stateKey);
|
|
4362
|
+
const result = zodSchema.safeParse(fullState);
|
|
4363
|
+
|
|
4364
|
+
const currentMeta = getShadowMetadata(stateKey, path) || {};
|
|
4365
|
+
|
|
4366
|
+
if (!result.success) {
|
|
4367
|
+
const errors =
|
|
4368
|
+
'issues' in result.error
|
|
4369
|
+
? result.error.issues
|
|
4370
|
+
: (result.error as any).errors;
|
|
4371
|
+
const pathErrors = errors.filter(
|
|
4372
|
+
(error: any) =>
|
|
4373
|
+
JSON.stringify(error.path) === JSON.stringify(path)
|
|
4374
|
+
);
|
|
4375
|
+
|
|
4376
|
+
if (pathErrors.length > 0) {
|
|
4377
|
+
setShadowMetadata(stateKey, path, {
|
|
4378
|
+
...currentMeta,
|
|
4379
|
+
validation: {
|
|
4380
|
+
status: 'INVALID_LIVE',
|
|
4381
|
+
message: pathErrors[0]?.message,
|
|
4382
|
+
validatedValue: newValue,
|
|
4383
|
+
},
|
|
4384
|
+
});
|
|
4385
|
+
} else {
|
|
4386
|
+
// This field has no errors - clear validation
|
|
4387
|
+
setShadowMetadata(stateKey, path, {
|
|
4388
|
+
...currentMeta,
|
|
4389
|
+
validation: {
|
|
4390
|
+
status: 'VALID_LIVE',
|
|
4391
|
+
validatedValue: newValue,
|
|
4392
|
+
},
|
|
4393
|
+
});
|
|
4394
|
+
}
|
|
4395
|
+
} else {
|
|
4396
|
+
// Validation passed - clear any existing errors
|
|
4397
|
+
setShadowMetadata(stateKey, path, {
|
|
4398
|
+
...currentMeta,
|
|
4399
|
+
validation: {
|
|
4400
|
+
status: 'VALID_LIVE',
|
|
4401
|
+
validatedValue: newValue,
|
|
4402
|
+
},
|
|
4403
|
+
});
|
|
4404
|
+
}
|
|
4405
|
+
}
|
|
4276
4406
|
}, debounceTime);
|
|
4407
|
+
forceUpdate({});
|
|
4277
4408
|
},
|
|
4278
|
-
[setState, path, formOpts?.debounceTime]
|
|
4409
|
+
[setState, path, formOpts?.debounceTime, stateKey]
|
|
4279
4410
|
);
|
|
4280
4411
|
|
|
4281
|
-
|
|
4412
|
+
// --- NEW onBlur HANDLER ---
|
|
4413
|
+
// This replaces the old commented-out method with a modern approach.
|
|
4414
|
+
const handleBlur = useCallback(async () => {
|
|
4415
|
+
console.log('handleBlur triggered');
|
|
4416
|
+
|
|
4417
|
+
// Commit any pending changes
|
|
4282
4418
|
if (debounceTimeoutRef.current) {
|
|
4283
4419
|
clearTimeout(debounceTimeoutRef.current);
|
|
4420
|
+
debounceTimeoutRef.current = null;
|
|
4284
4421
|
isCurrentlyDebouncing.current = false;
|
|
4285
4422
|
setState(localValue, path, { updateType: 'update' });
|
|
4286
4423
|
}
|
|
4287
|
-
|
|
4424
|
+
|
|
4425
|
+
const { getInitialOptions } = getGlobalStore.getState();
|
|
4426
|
+
const validationOptions = getInitialOptions(stateKey)?.validation;
|
|
4427
|
+
const zodSchema =
|
|
4428
|
+
validationOptions?.zodSchemaV4 || validationOptions?.zodSchemaV3;
|
|
4429
|
+
|
|
4430
|
+
if (!zodSchema) return;
|
|
4431
|
+
|
|
4432
|
+
// Get the full path including stateKey
|
|
4433
|
+
|
|
4434
|
+
// Update validation state to "validating"
|
|
4435
|
+
const currentMeta = getGlobalStore
|
|
4436
|
+
.getState()
|
|
4437
|
+
.getShadowMetadata(stateKey, path);
|
|
4438
|
+
getGlobalStore.getState().setShadowMetadata(stateKey, path, {
|
|
4439
|
+
...currentMeta,
|
|
4440
|
+
validation: {
|
|
4441
|
+
status: 'DIRTY',
|
|
4442
|
+
validatedValue: localValue,
|
|
4443
|
+
},
|
|
4444
|
+
});
|
|
4445
|
+
|
|
4446
|
+
// Validate full state
|
|
4447
|
+
const fullState = getGlobalStore.getState().getShadowValue(stateKey);
|
|
4448
|
+
const result = zodSchema.safeParse(fullState);
|
|
4449
|
+
console.log('result ', result);
|
|
4450
|
+
if (!result.success) {
|
|
4451
|
+
const errors =
|
|
4452
|
+
'issues' in result.error
|
|
4453
|
+
? result.error.issues
|
|
4454
|
+
: (result.error as any).errors;
|
|
4455
|
+
|
|
4456
|
+
console.log('All validation errors:', errors);
|
|
4457
|
+
console.log('Current blur path:', path);
|
|
4458
|
+
|
|
4459
|
+
// Find errors for this specific path
|
|
4460
|
+
const pathErrors = errors.filter((error: any) => {
|
|
4461
|
+
console.log('Processing error:', error);
|
|
4462
|
+
|
|
4463
|
+
// For array paths, we need to translate indices to ULIDs
|
|
4464
|
+
if (path.some((p) => p.startsWith('id:'))) {
|
|
4465
|
+
console.log('Detected array path with ULID');
|
|
4466
|
+
|
|
4467
|
+
// This is an array item path like ["id:xyz", "name"]
|
|
4468
|
+
const parentPath = path[0]!.startsWith('id:')
|
|
4469
|
+
? []
|
|
4470
|
+
: path.slice(0, -1);
|
|
4471
|
+
|
|
4472
|
+
console.log('Parent path:', parentPath);
|
|
4473
|
+
|
|
4474
|
+
const arrayMeta = getGlobalStore
|
|
4475
|
+
.getState()
|
|
4476
|
+
.getShadowMetadata(stateKey, parentPath);
|
|
4477
|
+
|
|
4478
|
+
console.log('Array metadata:', arrayMeta);
|
|
4479
|
+
|
|
4480
|
+
if (arrayMeta?.arrayKeys) {
|
|
4481
|
+
const itemKey = [stateKey, ...path.slice(0, -1)].join('.');
|
|
4482
|
+
const itemIndex = arrayMeta.arrayKeys.indexOf(itemKey);
|
|
4483
|
+
|
|
4484
|
+
console.log('Item key:', itemKey, 'Index:', itemIndex);
|
|
4485
|
+
|
|
4486
|
+
// Compare with Zod path
|
|
4487
|
+
const zodPath = [...parentPath, itemIndex, ...path.slice(-1)];
|
|
4488
|
+
const match =
|
|
4489
|
+
JSON.stringify(error.path) === JSON.stringify(zodPath);
|
|
4490
|
+
|
|
4491
|
+
console.log('Zod path comparison:', {
|
|
4492
|
+
zodPath,
|
|
4493
|
+
errorPath: error.path,
|
|
4494
|
+
match,
|
|
4495
|
+
});
|
|
4496
|
+
return match;
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
|
|
4500
|
+
const directMatch = JSON.stringify(error.path) === JSON.stringify(path);
|
|
4501
|
+
console.log('Direct path comparison:', {
|
|
4502
|
+
errorPath: error.path,
|
|
4503
|
+
currentPath: path,
|
|
4504
|
+
match: directMatch,
|
|
4505
|
+
});
|
|
4506
|
+
return directMatch;
|
|
4507
|
+
});
|
|
4508
|
+
|
|
4509
|
+
console.log('Filtered path errors:', pathErrors);
|
|
4510
|
+
// Update shadow metadata with validation result
|
|
4511
|
+
getGlobalStore.getState().setShadowMetadata(stateKey, path, {
|
|
4512
|
+
...currentMeta,
|
|
4513
|
+
validation: {
|
|
4514
|
+
status: 'VALIDATION_FAILED',
|
|
4515
|
+
message: pathErrors[0]?.message,
|
|
4516
|
+
validatedValue: localValue,
|
|
4517
|
+
},
|
|
4518
|
+
});
|
|
4519
|
+
} else {
|
|
4520
|
+
// Validation passed
|
|
4521
|
+
getGlobalStore.getState().setShadowMetadata(stateKey, path, {
|
|
4522
|
+
...currentMeta,
|
|
4523
|
+
validation: {
|
|
4524
|
+
status: 'VALID_PENDING_SYNC',
|
|
4525
|
+
validatedValue: localValue,
|
|
4526
|
+
},
|
|
4527
|
+
});
|
|
4528
|
+
}
|
|
4529
|
+
forceUpdate({});
|
|
4530
|
+
}, [stateKey, path, localValue, setState]);
|
|
4288
4531
|
|
|
4289
4532
|
const baseState = rebuildStateShape({
|
|
4290
4533
|
currentState: globalStateValue,
|
|
@@ -4300,7 +4543,8 @@ function FormElementWrapper({
|
|
|
4300
4543
|
onChange: (e: any) => {
|
|
4301
4544
|
debouncedUpdate(e.target.value);
|
|
4302
4545
|
},
|
|
4303
|
-
onBlur
|
|
4546
|
+
// 5. Wire the new onBlur handler to the input props.
|
|
4547
|
+
onBlur: handleBlur,
|
|
4304
4548
|
ref: formRefStore
|
|
4305
4549
|
.getState()
|
|
4306
4550
|
.getFormRef(stateKey + '.' + path.join('.')),
|
|
@@ -4311,9 +4555,12 @@ function FormElementWrapper({
|
|
|
4311
4555
|
},
|
|
4312
4556
|
});
|
|
4313
4557
|
|
|
4314
|
-
return
|
|
4558
|
+
return (
|
|
4559
|
+
<ValidationWrapper formOpts={formOpts} path={path} stateKey={stateKey}>
|
|
4560
|
+
{renderFn(stateWithInputProps)}
|
|
4561
|
+
</ValidationWrapper>
|
|
4562
|
+
);
|
|
4315
4563
|
}
|
|
4316
|
-
|
|
4317
4564
|
function useRegisterComponent(
|
|
4318
4565
|
stateKey: string,
|
|
4319
4566
|
componentId: string,
|
|
@@ -4322,25 +4569,19 @@ function useRegisterComponent(
|
|
|
4322
4569
|
const fullComponentId = `${stateKey}////${componentId}`;
|
|
4323
4570
|
|
|
4324
4571
|
useLayoutEffect(() => {
|
|
4325
|
-
const
|
|
4326
|
-
|
|
4572
|
+
const { registerComponent, unregisterComponent } =
|
|
4573
|
+
getGlobalStore.getState();
|
|
4327
4574
|
|
|
4328
|
-
|
|
4575
|
+
// Call the safe, centralized function to register
|
|
4576
|
+
registerComponent(stateKey, fullComponentId, {
|
|
4329
4577
|
forceUpdate: () => forceUpdate({}),
|
|
4330
4578
|
paths: new Set(),
|
|
4331
4579
|
reactiveType: ['component'],
|
|
4332
4580
|
});
|
|
4333
4581
|
|
|
4334
|
-
|
|
4335
|
-
...rootMeta,
|
|
4336
|
-
components,
|
|
4337
|
-
});
|
|
4338
|
-
|
|
4582
|
+
// The cleanup now calls the safe, centralized unregister function
|
|
4339
4583
|
return () => {
|
|
4340
|
-
|
|
4341
|
-
if (meta?.components) {
|
|
4342
|
-
meta.components.delete(fullComponentId);
|
|
4343
|
-
}
|
|
4584
|
+
unregisterComponent(stateKey, fullComponentId);
|
|
4344
4585
|
};
|
|
4345
|
-
}, [stateKey, fullComponentId]);
|
|
4586
|
+
}, [stateKey, fullComponentId]); // Dependencies are stable and correct
|
|
4346
4587
|
}
|