cogsbox-state 0.5.461 → 0.5.463
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 +3 -4
- package/dist/CogsState.d.ts.map +1 -1
- package/dist/CogsState.jsx +1489 -1505
- package/dist/CogsState.jsx.map +1 -1
- package/dist/store.d.ts +5 -4
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +231 -196
- package/dist/store.js.map +1 -1
- package/package.json +1 -1
- package/src/CogsState.tsx +1136 -1138
- package/src/store.ts +165 -134
package/src/CogsState.tsx
CHANGED
|
@@ -284,7 +284,7 @@ export type EndType<T, IsArrayElement = false> = {
|
|
|
284
284
|
hideMessage?: boolean;
|
|
285
285
|
}) => JSX.Element;
|
|
286
286
|
lastSynced?: SyncInfo;
|
|
287
|
-
} & (IsArrayElement extends true ? {
|
|
287
|
+
} & (IsArrayElement extends true ? { cutThis: () => void } : {});
|
|
288
288
|
|
|
289
289
|
export type StateObject<T> = (T extends any[]
|
|
290
290
|
? ArrayEndType<T>
|
|
@@ -419,13 +419,7 @@ export type OptionsType<T extends unknown = unknown, TApiParams = never> = {
|
|
|
419
419
|
onSuccess?: (data: any) => void;
|
|
420
420
|
onError?: (error: any) => void;
|
|
421
421
|
};
|
|
422
|
-
middleware?: ({
|
|
423
|
-
updateLog,
|
|
424
|
-
update,
|
|
425
|
-
}: {
|
|
426
|
-
updateLog: UpdateTypeDetail[] | undefined;
|
|
427
|
-
update: UpdateTypeDetail;
|
|
428
|
-
}) => void;
|
|
422
|
+
middleware?: ({ update }: { update: UpdateTypeDetail }) => void;
|
|
429
423
|
|
|
430
424
|
modifyState?: (state: T) => T;
|
|
431
425
|
localStorage?: {
|
|
@@ -578,26 +572,21 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
|
|
|
578
572
|
}
|
|
579
573
|
) => {
|
|
580
574
|
let newInitialState = initialState;
|
|
581
|
-
console.log('optsc', opt?.__useSync);
|
|
582
575
|
const [statePart, initialOptionsPart] =
|
|
583
576
|
transformStateFunc<State>(newInitialState);
|
|
584
577
|
|
|
585
|
-
// Store notifications if provided
|
|
586
578
|
if (opt?.__fromSyncSchema && opt?.__syncNotifications) {
|
|
587
579
|
getGlobalStore
|
|
588
580
|
.getState()
|
|
589
581
|
.setInitialStateOptions('__notifications', opt.__syncNotifications);
|
|
590
582
|
}
|
|
591
583
|
|
|
592
|
-
// Store apiParams map if provided
|
|
593
584
|
if (opt?.__fromSyncSchema && opt?.__apiParamsMap) {
|
|
594
585
|
getGlobalStore
|
|
595
586
|
.getState()
|
|
596
587
|
.setInitialStateOptions('__apiParamsMap', opt.__apiParamsMap);
|
|
597
588
|
}
|
|
598
589
|
|
|
599
|
-
// ... rest of your existing createCogsState code unchanged ...
|
|
600
|
-
|
|
601
590
|
Object.keys(statePart).forEach((key) => {
|
|
602
591
|
let existingOptions = initialOptionsPart[key] || {};
|
|
603
592
|
|
|
@@ -653,6 +642,7 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
|
|
|
653
642
|
stateKey: StateKey,
|
|
654
643
|
options?: Prettify<OptionsType<(typeof statePart)[StateKey]>>
|
|
655
644
|
) => {
|
|
645
|
+
console.time('useCogsState');
|
|
656
646
|
const [componentId] = useState(options?.componentId ?? uuidv4());
|
|
657
647
|
|
|
658
648
|
setOptions({
|
|
@@ -667,6 +657,8 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
|
|
|
667
657
|
? options.modifyState(thiState)
|
|
668
658
|
: thiState;
|
|
669
659
|
|
|
660
|
+
console.timeEnd('useCogsState');
|
|
661
|
+
|
|
670
662
|
const updater = useCogsStateFn<(typeof statePart)[StateKey]>(partialState, {
|
|
671
663
|
stateKey: stateKey as string,
|
|
672
664
|
syncUpdate: options?.syncUpdate,
|
|
@@ -788,7 +780,7 @@ export function createCogsStateFromSync<
|
|
|
788
780
|
const {
|
|
789
781
|
getInitialOptions,
|
|
790
782
|
|
|
791
|
-
|
|
783
|
+
addStateLog,
|
|
792
784
|
updateInitialStateGlobal,
|
|
793
785
|
} = getGlobalStore.getState();
|
|
794
786
|
const saveToLocalStorage = <T,>(
|
|
@@ -978,6 +970,8 @@ function markEntireStateAsServerSynced(
|
|
|
978
970
|
});
|
|
979
971
|
}
|
|
980
972
|
}
|
|
973
|
+
let updateBatchQueue = new Map<string, Array<UpdateArg<any>>>();
|
|
974
|
+
let batchFlushScheduled = false;
|
|
981
975
|
|
|
982
976
|
export function useCogsStateFn<TStateObject extends unknown>(
|
|
983
977
|
stateObject: TStateObject,
|
|
@@ -994,7 +988,6 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
994
988
|
dependencies,
|
|
995
989
|
serverState,
|
|
996
990
|
__useSync,
|
|
997
|
-
syncOptions,
|
|
998
991
|
}: {
|
|
999
992
|
stateKey?: string;
|
|
1000
993
|
componentId?: string;
|
|
@@ -1003,13 +996,11 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1003
996
|
syncOptions?: SyncOptionsType<any>;
|
|
1004
997
|
} & OptionsType<TStateObject> = {}
|
|
1005
998
|
) {
|
|
999
|
+
console.time('useCogsStateFn top');
|
|
1006
1000
|
const [reactiveForce, forceUpdate] = useState({}); //this is the key to reactivity
|
|
1007
1001
|
const { sessionId } = useCogsConfig();
|
|
1008
|
-
|
|
1009
1002
|
let noStateKey = stateKey ? false : true;
|
|
1010
1003
|
const [thisKey] = useState(stateKey ?? uuidv4());
|
|
1011
|
-
const stateLog = getGlobalStore.getState().stateLog[thisKey];
|
|
1012
|
-
const componentUpdatesRef = useRef(new Set<string>());
|
|
1013
1004
|
const componentIdRef = useRef(componentId ?? uuidv4());
|
|
1014
1005
|
const latestInitialOptionsRef = useRef<OptionsType<TStateObject> | null>(
|
|
1015
1006
|
null
|
|
@@ -1019,9 +1010,6 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1019
1010
|
|
|
1020
1011
|
useEffect(() => {
|
|
1021
1012
|
if (syncUpdate && syncUpdate.stateKey === thisKey && syncUpdate.path?.[0]) {
|
|
1022
|
-
// Update the actual state value
|
|
1023
|
-
|
|
1024
|
-
// Create combined key and update sync info
|
|
1025
1013
|
const syncKey = `${syncUpdate.stateKey}:${syncUpdate.path.join('.')}`;
|
|
1026
1014
|
getGlobalStore.getState().setSyncInfo(syncKey, {
|
|
1027
1015
|
timeStamp: syncUpdate.timeStamp!,
|
|
@@ -1254,6 +1242,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1254
1242
|
notifyComponents(thisKey);
|
|
1255
1243
|
}
|
|
1256
1244
|
}, [thisKey, ...(dependencies || [])]);
|
|
1245
|
+
|
|
1257
1246
|
useLayoutEffect(() => {
|
|
1258
1247
|
if (noStateKey) {
|
|
1259
1248
|
setAndMergeOptions(thisKey as string, {
|
|
@@ -1323,22 +1312,22 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1323
1312
|
}, []);
|
|
1324
1313
|
|
|
1325
1314
|
const syncApiRef = useRef<SyncApi | null>(null);
|
|
1315
|
+
console.timeEnd('useCogsStateFn top');
|
|
1316
|
+
|
|
1326
1317
|
const effectiveSetState = (
|
|
1327
1318
|
newStateOrFunction: UpdateArg<TStateObject> | InsertParams<TStateObject>,
|
|
1328
1319
|
path: string[],
|
|
1329
1320
|
updateObj: UpdateOptions
|
|
1330
1321
|
) => {
|
|
1322
|
+
console.time('top of effectiveSetState');
|
|
1331
1323
|
const fullPath = [thisKey, ...path].join('.');
|
|
1332
|
-
if (Array.isArray(path)) {
|
|
1333
|
-
const pathKey = `${thisKey}-${path.join('.')}`;
|
|
1334
|
-
componentUpdatesRef.current.add(pathKey);
|
|
1335
|
-
}
|
|
1336
1324
|
const store = getGlobalStore.getState();
|
|
1337
1325
|
|
|
1338
|
-
// FETCH ONCE at the beginning
|
|
1339
1326
|
const shadowMeta = store.getShadowMetadata(thisKey, path);
|
|
1340
1327
|
const nestedShadowValue = store.getShadowValue(fullPath) as TStateObject;
|
|
1328
|
+
console.timeEnd('top of effectiveSetState');
|
|
1341
1329
|
|
|
1330
|
+
console.time('top of payload');
|
|
1342
1331
|
const payload = (
|
|
1343
1332
|
updateObj.updateType === 'insert' && isFunction(newStateOrFunction)
|
|
1344
1333
|
? newStateOrFunction({ state: nestedShadowValue, uuid: uuidv4() })
|
|
@@ -1358,17 +1347,15 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1358
1347
|
oldValue: nestedShadowValue,
|
|
1359
1348
|
newValue: payload,
|
|
1360
1349
|
} satisfies UpdateTypeDetail;
|
|
1350
|
+
console.timeEnd('top of payload');
|
|
1361
1351
|
|
|
1352
|
+
console.time('switch in effectiveSetState');
|
|
1362
1353
|
// Perform the update
|
|
1363
1354
|
switch (updateObj.updateType) {
|
|
1364
1355
|
case 'insert': {
|
|
1365
1356
|
store.insertShadowArrayElement(thisKey, path, newUpdate.newValue);
|
|
1366
|
-
// The array at `path` has been modified. Mark it AND all its parents as dirty.
|
|
1367
1357
|
store.markAsDirty(thisKey, path, { bubble: true });
|
|
1368
|
-
|
|
1369
|
-
// ALSO mark the newly inserted item itself as dirty
|
|
1370
|
-
// Get the new item's path and mark it as dirty
|
|
1371
|
-
const arrayMeta = store.getShadowMetadata(thisKey, path);
|
|
1358
|
+
const arrayMeta = shadowMeta;
|
|
1372
1359
|
if (arrayMeta?.arrayKeys) {
|
|
1373
1360
|
const newItemKey =
|
|
1374
1361
|
arrayMeta.arrayKeys[arrayMeta.arrayKeys.length - 1];
|
|
@@ -1392,7 +1379,9 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1392
1379
|
break;
|
|
1393
1380
|
}
|
|
1394
1381
|
}
|
|
1382
|
+
console.timeEnd('switch in effectiveSetState');
|
|
1395
1383
|
const shouldSync = updateObj.sync !== false;
|
|
1384
|
+
console.time('signals');
|
|
1396
1385
|
|
|
1397
1386
|
if (shouldSync && syncApiRef.current && syncApiRef.current.connected) {
|
|
1398
1387
|
syncApiRef.current.updateState({ operation: newUpdate });
|
|
@@ -1545,6 +1534,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1545
1534
|
});
|
|
1546
1535
|
}
|
|
1547
1536
|
}
|
|
1537
|
+
|
|
1548
1538
|
if (updateObj.updateType === 'cut') {
|
|
1549
1539
|
const arrayPath = path.slice(0, -1);
|
|
1550
1540
|
const arrayMeta = store.getShadowMetadata(thisKey, arrayPath);
|
|
@@ -1562,16 +1552,14 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1562
1552
|
});
|
|
1563
1553
|
}
|
|
1564
1554
|
}
|
|
1555
|
+
console.timeEnd('signals');
|
|
1565
1556
|
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
const newState = getGlobalStore.getState().getShadowValue(thisKey);
|
|
1570
|
-
const rootMeta = getGlobalStore.getState().getShadowMetadata(thisKey, []);
|
|
1557
|
+
console.time('notify');
|
|
1558
|
+
const rootMeta = store.getShadowMetadata(thisKey, []);
|
|
1571
1559
|
const notifiedComponents = new Set<string>();
|
|
1572
1560
|
|
|
1573
1561
|
if (!rootMeta?.components) {
|
|
1574
|
-
return
|
|
1562
|
+
return;
|
|
1575
1563
|
}
|
|
1576
1564
|
|
|
1577
1565
|
// --- PASS 1: Notify specific subscribers based on update type ---
|
|
@@ -1753,26 +1741,9 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1753
1741
|
}
|
|
1754
1742
|
});
|
|
1755
1743
|
notifiedComponents.clear();
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
logs.forEach((log) => {
|
|
1761
|
-
const uniqueKey = `${log.stateKey}:${JSON.stringify(log.path)}`;
|
|
1762
|
-
const existing = aggregatedLogs.get(uniqueKey);
|
|
1763
|
-
|
|
1764
|
-
if (existing) {
|
|
1765
|
-
existing.timeStamp = Math.max(existing.timeStamp, log.timeStamp);
|
|
1766
|
-
existing.newValue = log.newValue;
|
|
1767
|
-
existing.oldValue = existing.oldValue ?? log.oldValue;
|
|
1768
|
-
existing.updateType = log.updateType;
|
|
1769
|
-
} else {
|
|
1770
|
-
aggregatedLogs.set(uniqueKey, { ...(log as any) });
|
|
1771
|
-
}
|
|
1772
|
-
});
|
|
1773
|
-
|
|
1774
|
-
return Array.from(aggregatedLogs.values());
|
|
1775
|
-
});
|
|
1744
|
+
console.timeEnd('notify');
|
|
1745
|
+
console.time('end stuff');
|
|
1746
|
+
addStateLog(thisKey, newUpdate);
|
|
1776
1747
|
|
|
1777
1748
|
saveToLocalStorage(
|
|
1778
1749
|
payload,
|
|
@@ -1783,12 +1754,10 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1783
1754
|
|
|
1784
1755
|
if (latestInitialOptionsRef.current?.middleware) {
|
|
1785
1756
|
latestInitialOptionsRef.current!.middleware({
|
|
1786
|
-
updateLog: stateLog,
|
|
1787
1757
|
update: newUpdate,
|
|
1788
1758
|
});
|
|
1789
1759
|
}
|
|
1790
|
-
|
|
1791
|
-
return newState;
|
|
1760
|
+
console.timeEnd('end stuff');
|
|
1792
1761
|
};
|
|
1793
1762
|
|
|
1794
1763
|
if (!getGlobalStore.getState().initialStateGlobal[thisKey]) {
|
|
@@ -1796,12 +1765,15 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1796
1765
|
}
|
|
1797
1766
|
|
|
1798
1767
|
const updaterFinal = useMemo(() => {
|
|
1799
|
-
|
|
1768
|
+
console.time('createProxyHandler');
|
|
1769
|
+
const handler = createProxyHandler<TStateObject>(
|
|
1800
1770
|
thisKey,
|
|
1801
1771
|
effectiveSetState,
|
|
1802
1772
|
componentIdRef.current,
|
|
1803
1773
|
sessionId
|
|
1804
1774
|
);
|
|
1775
|
+
console.timeEnd('createProxyHandler'); // <--- AND THIS
|
|
1776
|
+
return handler;
|
|
1805
1777
|
}, [thisKey, sessionId]);
|
|
1806
1778
|
|
|
1807
1779
|
const cogsSyncFn = __useSync;
|
|
@@ -1848,12 +1820,9 @@ function hashTransforms(transforms: any[]) {
|
|
|
1848
1820
|
if (!transforms || transforms.length === 0) {
|
|
1849
1821
|
return '';
|
|
1850
1822
|
}
|
|
1851
|
-
// This creates a string representation of the transforms AND their dependencies.
|
|
1852
|
-
// Example: "filter['red']sort['score','asc']"
|
|
1853
1823
|
return transforms
|
|
1854
1824
|
.map(
|
|
1855
1825
|
(transform) =>
|
|
1856
|
-
// Safely stringify dependencies. An empty array becomes '[]'.
|
|
1857
1826
|
`${transform.type}${JSON.stringify(transform.dependencies || [])}`
|
|
1858
1827
|
)
|
|
1859
1828
|
.join('');
|
|
@@ -1896,8 +1865,6 @@ const registerComponentDependency = (
|
|
|
1896
1865
|
const fullComponentId = `${stateKey}////${componentId}`;
|
|
1897
1866
|
const { addPathComponent, getShadowMetadata } = getGlobalStore.getState();
|
|
1898
1867
|
|
|
1899
|
-
// First, check if the component should even be registered.
|
|
1900
|
-
// This check is safe to do outside the setter.
|
|
1901
1868
|
const rootMeta = getShadowMetadata(stateKey, []);
|
|
1902
1869
|
const component = rootMeta?.components?.get(fullComponentId);
|
|
1903
1870
|
|
|
@@ -1913,7 +1880,6 @@ const registerComponentDependency = (
|
|
|
1913
1880
|
return;
|
|
1914
1881
|
}
|
|
1915
1882
|
|
|
1916
|
-
// Now, call the single, safe, atomic function to perform the update.
|
|
1917
1883
|
addPathComponent(stateKey, dependencyPath, fullComponentId);
|
|
1918
1884
|
};
|
|
1919
1885
|
const notifySelectionComponents = (
|
|
@@ -1961,48 +1927,40 @@ const notifySelectionComponents = (
|
|
|
1961
1927
|
}
|
|
1962
1928
|
}
|
|
1963
1929
|
};
|
|
1930
|
+
|
|
1964
1931
|
function createProxyHandler<T>(
|
|
1965
1932
|
stateKey: string,
|
|
1966
1933
|
effectiveSetState: EffectiveSetState<T>,
|
|
1967
1934
|
componentId: string,
|
|
1968
1935
|
sessionId?: string
|
|
1969
1936
|
): StateObject<T> {
|
|
1970
|
-
|
|
1971
|
-
proxy: any;
|
|
1972
|
-
stateVersion: number;
|
|
1973
|
-
};
|
|
1974
|
-
|
|
1975
|
-
const shapeCache = new Map<string, CacheEntry>();
|
|
1937
|
+
const proxyCache = new Map<string, any>();
|
|
1976
1938
|
let stateVersion = 0;
|
|
1939
|
+
console.time('rebuildStateShape Outer');
|
|
1977
1940
|
|
|
1978
|
-
|
|
1979
|
-
const pathKey = path.join('.');
|
|
1980
|
-
for (const [key] of shapeCache) {
|
|
1981
|
-
if (key === pathKey || key.startsWith(pathKey + '.')) {
|
|
1982
|
-
shapeCache.delete(key);
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
1985
|
-
stateVersion++;
|
|
1986
|
-
};
|
|
1941
|
+
let recursionTimerName: string | null = null;
|
|
1987
1942
|
|
|
1988
1943
|
function rebuildStateShape({
|
|
1989
|
-
currentState,
|
|
1990
1944
|
path = [],
|
|
1991
1945
|
meta,
|
|
1992
1946
|
componentId,
|
|
1993
1947
|
}: {
|
|
1994
|
-
currentState: T;
|
|
1995
1948
|
path: string[];
|
|
1996
1949
|
componentId: string;
|
|
1997
1950
|
meta?: MetaData;
|
|
1998
1951
|
}): any {
|
|
1999
|
-
|
|
1952
|
+
console.time('rebuildStateShape Inner');
|
|
1953
|
+
const derivationSignature = meta
|
|
1954
|
+
? JSON.stringify(meta.validIds || meta.transforms)
|
|
1955
|
+
: '';
|
|
1956
|
+
const cacheKey = path.join('.') + ':' + derivationSignature;
|
|
1957
|
+
console.log('PROXY CACHE KEY ', cacheKey);
|
|
1958
|
+
if (proxyCache.has(cacheKey)) {
|
|
1959
|
+
console.log('PROXY CACHE HIT');
|
|
1960
|
+
return proxyCache.get(cacheKey);
|
|
1961
|
+
}
|
|
2000
1962
|
const stateKeyPathKey = [stateKey, ...path].join('.');
|
|
2001
1963
|
|
|
2002
|
-
currentState = getGlobalStore
|
|
2003
|
-
.getState()
|
|
2004
|
-
.getShadowValue(stateKeyPathKey, meta?.validIds);
|
|
2005
|
-
|
|
2006
1964
|
type CallableStateObject<T> = {
|
|
2007
1965
|
(): T;
|
|
2008
1966
|
} & {
|
|
@@ -2022,9 +1980,11 @@ function createProxyHandler<T>(
|
|
|
2022
1980
|
},
|
|
2023
1981
|
|
|
2024
1982
|
get(target: any, prop: string) {
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
1983
|
+
if (path.length === 0) {
|
|
1984
|
+
// Create a unique name for this specific timer instance
|
|
1985
|
+
recursionTimerName = `Recursion-${Math.random()}`;
|
|
1986
|
+
console.time(recursionTimerName);
|
|
1987
|
+
}
|
|
2028
1988
|
if (prop === '_rebuildStateShape') {
|
|
2029
1989
|
return rebuildStateShape;
|
|
2030
1990
|
}
|
|
@@ -2200,1167 +2160,1154 @@ function createProxyHandler<T>(
|
|
|
2200
2160
|
return [];
|
|
2201
2161
|
};
|
|
2202
2162
|
}
|
|
2203
|
-
if (Array.isArray(currentState)) {
|
|
2204
|
-
if (prop === 'getSelected') {
|
|
2205
|
-
return () => {
|
|
2206
|
-
const fullKey = stateKey + '.' + path.join('.');
|
|
2207
|
-
registerComponentDependency(stateKey, componentId, [
|
|
2208
|
-
...path,
|
|
2209
|
-
'getSelected',
|
|
2210
|
-
]);
|
|
2211
|
-
|
|
2212
|
-
const selectedIndicesMap =
|
|
2213
|
-
getGlobalStore.getState().selectedIndicesMap;
|
|
2214
|
-
if (!selectedIndicesMap || !selectedIndicesMap.has(fullKey)) {
|
|
2215
|
-
return undefined;
|
|
2216
|
-
}
|
|
2217
|
-
|
|
2218
|
-
const selectedItemKey = selectedIndicesMap.get(fullKey)!;
|
|
2219
|
-
if (meta?.validIds) {
|
|
2220
|
-
if (!meta.validIds.includes(selectedItemKey)) {
|
|
2221
|
-
return undefined;
|
|
2222
|
-
}
|
|
2223
|
-
}
|
|
2224
2163
|
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2164
|
+
if (prop === 'getSelected') {
|
|
2165
|
+
return () => {
|
|
2166
|
+
const fullKey = stateKey + '.' + path.join('.');
|
|
2167
|
+
registerComponentDependency(stateKey, componentId, [
|
|
2168
|
+
...path,
|
|
2169
|
+
'getSelected',
|
|
2170
|
+
]);
|
|
2171
|
+
|
|
2172
|
+
const selectedIndicesMap =
|
|
2173
|
+
getGlobalStore.getState().selectedIndicesMap;
|
|
2174
|
+
if (!selectedIndicesMap || !selectedIndicesMap.has(fullKey)) {
|
|
2175
|
+
return undefined;
|
|
2176
|
+
}
|
|
2228
2177
|
|
|
2229
|
-
|
|
2178
|
+
const selectedItemKey = selectedIndicesMap.get(fullKey)!;
|
|
2179
|
+
if (meta?.validIds) {
|
|
2180
|
+
if (!meta.validIds.includes(selectedItemKey)) {
|
|
2230
2181
|
return undefined;
|
|
2231
2182
|
}
|
|
2183
|
+
}
|
|
2232
2184
|
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
componentId: componentId!,
|
|
2237
|
-
});
|
|
2238
|
-
};
|
|
2239
|
-
}
|
|
2240
|
-
if (prop === 'getSelectedIndex') {
|
|
2241
|
-
return () => {
|
|
2242
|
-
const selectedIndex = getGlobalStore
|
|
2243
|
-
.getState()
|
|
2244
|
-
.getSelectedIndex(
|
|
2245
|
-
stateKey + '.' + path.join('.'),
|
|
2246
|
-
meta?.validIds
|
|
2247
|
-
);
|
|
2248
|
-
|
|
2249
|
-
return selectedIndex;
|
|
2250
|
-
};
|
|
2251
|
-
}
|
|
2252
|
-
if (prop === 'clearSelected') {
|
|
2253
|
-
notifySelectionComponents(stateKey, path);
|
|
2254
|
-
return () => {
|
|
2255
|
-
getGlobalStore.getState().clearSelectedIndex({
|
|
2256
|
-
arrayKey: stateKey + '.' + path.join('.'),
|
|
2257
|
-
});
|
|
2258
|
-
};
|
|
2259
|
-
}
|
|
2185
|
+
const value = getGlobalStore
|
|
2186
|
+
.getState()
|
|
2187
|
+
.getShadowValue(selectedItemKey);
|
|
2260
2188
|
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
): VirtualStateObjectResult<any[]> => {
|
|
2265
|
-
const {
|
|
2266
|
-
itemHeight = 50,
|
|
2267
|
-
overscan = 6,
|
|
2268
|
-
stickToBottom = false,
|
|
2269
|
-
scrollStickTolerance = 75,
|
|
2270
|
-
} = options;
|
|
2271
|
-
|
|
2272
|
-
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
2273
|
-
const [range, setRange] = useState({
|
|
2274
|
-
startIndex: 0,
|
|
2275
|
-
endIndex: 10,
|
|
2276
|
-
});
|
|
2277
|
-
const [rerender, forceUpdate] = useState({});
|
|
2278
|
-
const initialScrollRef = useRef(true);
|
|
2279
|
-
|
|
2280
|
-
// Scroll state management
|
|
2281
|
-
const scrollStateRef = useRef({
|
|
2282
|
-
isUserScrolling: false,
|
|
2283
|
-
lastScrollTop: 0,
|
|
2284
|
-
scrollUpCount: 0,
|
|
2285
|
-
isNearBottom: true,
|
|
2286
|
-
});
|
|
2189
|
+
if (!value) {
|
|
2190
|
+
return undefined;
|
|
2191
|
+
}
|
|
2287
2192
|
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2193
|
+
return rebuildStateShape({
|
|
2194
|
+
path: selectedItemKey.split('.').slice(1) as string[],
|
|
2195
|
+
componentId: componentId!,
|
|
2196
|
+
});
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
2199
|
+
if (prop === 'getSelectedIndex') {
|
|
2200
|
+
return () => {
|
|
2201
|
+
const selectedIndex = getGlobalStore
|
|
2202
|
+
.getState()
|
|
2203
|
+
.getSelectedIndex(
|
|
2204
|
+
stateKey + '.' + path.join('.'),
|
|
2205
|
+
meta?.validIds
|
|
2291
2206
|
);
|
|
2292
2207
|
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2208
|
+
return selectedIndex;
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2211
|
+
if (prop === 'clearSelected') {
|
|
2212
|
+
notifySelectionComponents(stateKey, path);
|
|
2213
|
+
return () => {
|
|
2214
|
+
getGlobalStore.getState().clearSelectedIndex({
|
|
2215
|
+
arrayKey: stateKey + '.' + path.join('.'),
|
|
2216
|
+
});
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2301
2219
|
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2220
|
+
if (prop === 'useVirtualView') {
|
|
2221
|
+
return (
|
|
2222
|
+
options: VirtualViewOptions
|
|
2223
|
+
): VirtualStateObjectResult<any[]> => {
|
|
2224
|
+
const {
|
|
2225
|
+
itemHeight = 50,
|
|
2226
|
+
overscan = 6,
|
|
2227
|
+
stickToBottom = false,
|
|
2228
|
+
scrollStickTolerance = 75,
|
|
2229
|
+
} = options;
|
|
2230
|
+
|
|
2231
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
2232
|
+
const [range, setRange] = useState({
|
|
2233
|
+
startIndex: 0,
|
|
2234
|
+
endIndex: 10,
|
|
2235
|
+
});
|
|
2236
|
+
const [rerender, forceUpdate] = useState({});
|
|
2237
|
+
const initialScrollRef = useRef(true);
|
|
2238
|
+
|
|
2239
|
+
// Scroll state management
|
|
2240
|
+
const scrollStateRef = useRef({
|
|
2241
|
+
isUserScrolling: false,
|
|
2242
|
+
lastScrollTop: 0,
|
|
2243
|
+
scrollUpCount: 0,
|
|
2244
|
+
isNearBottom: true,
|
|
2245
|
+
});
|
|
2246
|
+
|
|
2247
|
+
// Measurement cache
|
|
2248
|
+
const measurementCache = useRef(
|
|
2249
|
+
new Map<string, { height: number; offset: number }>()
|
|
2250
|
+
);
|
|
2251
|
+
|
|
2252
|
+
// Separate effect for handling rerender updates
|
|
2253
|
+
useLayoutEffect(() => {
|
|
2254
|
+
if (
|
|
2255
|
+
!stickToBottom ||
|
|
2256
|
+
!containerRef.current ||
|
|
2257
|
+
scrollStateRef.current.isUserScrolling
|
|
2258
|
+
)
|
|
2259
|
+
return;
|
|
2308
2260
|
|
|
2309
|
-
const
|
|
2261
|
+
const container = containerRef.current;
|
|
2262
|
+
container.scrollTo({
|
|
2263
|
+
top: container.scrollHeight,
|
|
2264
|
+
behavior: initialScrollRef.current ? 'instant' : 'smooth',
|
|
2265
|
+
});
|
|
2266
|
+
}, [rerender, stickToBottom]);
|
|
2267
|
+
|
|
2268
|
+
const arrayKeys =
|
|
2269
|
+
getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
2270
|
+
?.arrayKeys || [];
|
|
2271
|
+
|
|
2272
|
+
// Calculate total height and offsets
|
|
2273
|
+
const { totalHeight, itemOffsets } = useMemo(() => {
|
|
2274
|
+
let runningOffset = 0;
|
|
2275
|
+
const offsets = new Map<
|
|
2276
|
+
string,
|
|
2277
|
+
{ height: number; offset: number }
|
|
2278
|
+
>();
|
|
2279
|
+
const allItemKeys =
|
|
2310
2280
|
getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
2311
2281
|
?.arrayKeys || [];
|
|
2312
2282
|
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
const allItemKeys =
|
|
2321
|
-
getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
2322
|
-
?.arrayKeys || [];
|
|
2323
|
-
|
|
2324
|
-
allItemKeys.forEach((itemKey) => {
|
|
2325
|
-
const itemPath = itemKey.split('.').slice(1);
|
|
2326
|
-
const measuredHeight =
|
|
2327
|
-
getGlobalStore
|
|
2328
|
-
.getState()
|
|
2329
|
-
.getShadowMetadata(stateKey, itemPath)?.virtualizer
|
|
2330
|
-
?.itemHeight || itemHeight;
|
|
2331
|
-
|
|
2332
|
-
offsets.set(itemKey, {
|
|
2333
|
-
height: measuredHeight,
|
|
2334
|
-
offset: runningOffset,
|
|
2335
|
-
});
|
|
2283
|
+
allItemKeys.forEach((itemKey) => {
|
|
2284
|
+
const itemPath = itemKey.split('.').slice(1);
|
|
2285
|
+
const measuredHeight =
|
|
2286
|
+
getGlobalStore
|
|
2287
|
+
.getState()
|
|
2288
|
+
.getShadowMetadata(stateKey, itemPath)?.virtualizer
|
|
2289
|
+
?.itemHeight || itemHeight;
|
|
2336
2290
|
|
|
2337
|
-
|
|
2291
|
+
offsets.set(itemKey, {
|
|
2292
|
+
height: measuredHeight,
|
|
2293
|
+
offset: runningOffset,
|
|
2338
2294
|
});
|
|
2339
2295
|
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
}, [arrayKeys.length, itemHeight]);
|
|
2343
|
-
|
|
2344
|
-
// Improved initial positioning effect
|
|
2345
|
-
useLayoutEffect(() => {
|
|
2346
|
-
if (
|
|
2347
|
-
stickToBottom &&
|
|
2348
|
-
arrayKeys.length > 0 &&
|
|
2349
|
-
containerRef.current &&
|
|
2350
|
-
!scrollStateRef.current.isUserScrolling &&
|
|
2351
|
-
initialScrollRef.current
|
|
2352
|
-
) {
|
|
2353
|
-
const container = containerRef.current;
|
|
2354
|
-
|
|
2355
|
-
// Wait for container to have dimensions
|
|
2356
|
-
const waitForContainer = () => {
|
|
2357
|
-
if (container.clientHeight > 0) {
|
|
2358
|
-
const visibleCount = Math.ceil(
|
|
2359
|
-
container.clientHeight / itemHeight
|
|
2360
|
-
);
|
|
2361
|
-
const endIndex = arrayKeys.length - 1;
|
|
2362
|
-
const startIndex = Math.max(
|
|
2363
|
-
0,
|
|
2364
|
-
endIndex - visibleCount - overscan
|
|
2365
|
-
);
|
|
2366
|
-
|
|
2367
|
-
setRange({ startIndex, endIndex });
|
|
2368
|
-
|
|
2369
|
-
// Ensure scroll after range is set
|
|
2370
|
-
requestAnimationFrame(() => {
|
|
2371
|
-
scrollToBottom('instant');
|
|
2372
|
-
initialScrollRef.current = false; // Mark initial scroll as done
|
|
2373
|
-
});
|
|
2374
|
-
} else {
|
|
2375
|
-
// Container not ready, try again
|
|
2376
|
-
requestAnimationFrame(waitForContainer);
|
|
2377
|
-
}
|
|
2378
|
-
};
|
|
2296
|
+
runningOffset += measuredHeight;
|
|
2297
|
+
});
|
|
2379
2298
|
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2299
|
+
measurementCache.current = offsets;
|
|
2300
|
+
return { totalHeight: runningOffset, itemOffsets: offsets };
|
|
2301
|
+
}, [arrayKeys.length, itemHeight]);
|
|
2383
2302
|
|
|
2384
|
-
|
|
2385
|
-
|
|
2303
|
+
// Improved initial positioning effect
|
|
2304
|
+
useLayoutEffect(() => {
|
|
2305
|
+
if (
|
|
2306
|
+
stickToBottom &&
|
|
2307
|
+
arrayKeys.length > 0 &&
|
|
2308
|
+
containerRef.current &&
|
|
2309
|
+
!scrollStateRef.current.isUserScrolling &&
|
|
2310
|
+
initialScrollRef.current
|
|
2311
|
+
) {
|
|
2386
2312
|
const container = containerRef.current;
|
|
2387
|
-
if (!container) return;
|
|
2388
2313
|
|
|
2389
|
-
|
|
2390
|
-
const
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
// Detect scroll direction
|
|
2401
|
-
if (currentScrollTop < scrollState.lastScrollTop) {
|
|
2402
|
-
// User scrolled up
|
|
2403
|
-
scrollState.scrollUpCount++;
|
|
2404
|
-
|
|
2405
|
-
if (scrollState.scrollUpCount > 3 && wasNearBottom) {
|
|
2406
|
-
// User has deliberately scrolled away from bottom
|
|
2407
|
-
scrollState.isUserScrolling = true;
|
|
2408
|
-
console.log('User scrolled away from bottom');
|
|
2409
|
-
}
|
|
2410
|
-
} else if (scrollState.isNearBottom) {
|
|
2411
|
-
// Reset if we're back near the bottom
|
|
2412
|
-
scrollState.isUserScrolling = false;
|
|
2413
|
-
scrollState.scrollUpCount = 0;
|
|
2414
|
-
}
|
|
2314
|
+
// Wait for container to have dimensions
|
|
2315
|
+
const waitForContainer = () => {
|
|
2316
|
+
if (container.clientHeight > 0) {
|
|
2317
|
+
const visibleCount = Math.ceil(
|
|
2318
|
+
container.clientHeight / itemHeight
|
|
2319
|
+
);
|
|
2320
|
+
const endIndex = arrayKeys.length - 1;
|
|
2321
|
+
const startIndex = Math.max(
|
|
2322
|
+
0,
|
|
2323
|
+
endIndex - visibleCount - overscan
|
|
2324
|
+
);
|
|
2415
2325
|
|
|
2416
|
-
|
|
2326
|
+
setRange({ startIndex, endIndex });
|
|
2417
2327
|
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2328
|
+
// Ensure scroll after range is set
|
|
2329
|
+
requestAnimationFrame(() => {
|
|
2330
|
+
scrollToBottom('instant');
|
|
2331
|
+
initialScrollRef.current = false; // Mark initial scroll as done
|
|
2332
|
+
});
|
|
2333
|
+
} else {
|
|
2334
|
+
// Container not ready, try again
|
|
2335
|
+
requestAnimationFrame(waitForContainer);
|
|
2426
2336
|
}
|
|
2427
|
-
}
|
|
2337
|
+
};
|
|
2428
2338
|
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2339
|
+
waitForContainer();
|
|
2340
|
+
}
|
|
2341
|
+
}, [arrayKeys.length, stickToBottom, itemHeight, overscan]);
|
|
2342
|
+
|
|
2343
|
+
// Combined scroll handler
|
|
2344
|
+
const handleScroll = useCallback(() => {
|
|
2345
|
+
const container = containerRef.current;
|
|
2346
|
+
if (!container) return;
|
|
2347
|
+
|
|
2348
|
+
const currentScrollTop = container.scrollTop;
|
|
2349
|
+
const { scrollHeight, clientHeight } = container;
|
|
2350
|
+
const scrollState = scrollStateRef.current;
|
|
2351
|
+
|
|
2352
|
+
// Check if user is near bottom
|
|
2353
|
+
const distanceFromBottom =
|
|
2354
|
+
scrollHeight - (currentScrollTop + clientHeight);
|
|
2355
|
+
const wasNearBottom = scrollState.isNearBottom;
|
|
2356
|
+
scrollState.isNearBottom =
|
|
2357
|
+
distanceFromBottom <= scrollStickTolerance;
|
|
2358
|
+
|
|
2359
|
+
// Detect scroll direction
|
|
2360
|
+
if (currentScrollTop < scrollState.lastScrollTop) {
|
|
2361
|
+
// User scrolled up
|
|
2362
|
+
scrollState.scrollUpCount++;
|
|
2363
|
+
|
|
2364
|
+
if (scrollState.scrollUpCount > 3 && wasNearBottom) {
|
|
2365
|
+
// User has deliberately scrolled away from bottom
|
|
2366
|
+
scrollState.isUserScrolling = true;
|
|
2367
|
+
console.log('User scrolled away from bottom');
|
|
2439
2368
|
}
|
|
2440
|
-
}
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
scrollStickTolerance,
|
|
2446
|
-
]);
|
|
2369
|
+
} else if (scrollState.isNearBottom) {
|
|
2370
|
+
// Reset if we're back near the bottom
|
|
2371
|
+
scrollState.isUserScrolling = false;
|
|
2372
|
+
scrollState.scrollUpCount = 0;
|
|
2373
|
+
}
|
|
2447
2374
|
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2375
|
+
scrollState.lastScrollTop = currentScrollTop;
|
|
2376
|
+
|
|
2377
|
+
// Update visible range
|
|
2378
|
+
let newStartIndex = 0;
|
|
2379
|
+
for (let i = 0; i < arrayKeys.length; i++) {
|
|
2380
|
+
const itemKey = arrayKeys[i];
|
|
2381
|
+
const item = measurementCache.current.get(itemKey!);
|
|
2382
|
+
if (item && item.offset + item.height > currentScrollTop) {
|
|
2383
|
+
newStartIndex = i;
|
|
2384
|
+
break;
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2452
2387
|
|
|
2453
|
-
|
|
2454
|
-
|
|
2388
|
+
// Only update if range actually changed
|
|
2389
|
+
if (newStartIndex !== range.startIndex) {
|
|
2390
|
+
const visibleCount = Math.ceil(clientHeight / itemHeight);
|
|
2391
|
+
setRange({
|
|
2392
|
+
startIndex: Math.max(0, newStartIndex - overscan),
|
|
2393
|
+
endIndex: Math.min(
|
|
2394
|
+
arrayKeys.length - 1,
|
|
2395
|
+
newStartIndex + visibleCount + overscan
|
|
2396
|
+
),
|
|
2455
2397
|
});
|
|
2398
|
+
}
|
|
2399
|
+
}, [
|
|
2400
|
+
arrayKeys.length,
|
|
2401
|
+
range.startIndex,
|
|
2402
|
+
itemHeight,
|
|
2403
|
+
overscan,
|
|
2404
|
+
scrollStickTolerance,
|
|
2405
|
+
]);
|
|
2406
|
+
|
|
2407
|
+
// Set up scroll listener
|
|
2408
|
+
useEffect(() => {
|
|
2409
|
+
const container = containerRef.current;
|
|
2410
|
+
if (!container || !stickToBottom) return;
|
|
2411
|
+
|
|
2412
|
+
container.addEventListener('scroll', handleScroll, {
|
|
2413
|
+
passive: true,
|
|
2414
|
+
});
|
|
2456
2415
|
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
// Reset scroll state
|
|
2467
|
-
scrollStateRef.current.isUserScrolling = false;
|
|
2468
|
-
scrollStateRef.current.isNearBottom = true;
|
|
2469
|
-
scrollStateRef.current.scrollUpCount = 0;
|
|
2470
|
-
|
|
2471
|
-
const performScroll = () => {
|
|
2472
|
-
// Multiple attempts to ensure we hit the bottom
|
|
2473
|
-
const attemptScroll = (attempts = 0) => {
|
|
2474
|
-
if (attempts > 5) return; // Prevent infinite loops
|
|
2475
|
-
|
|
2476
|
-
const currentHeight = container.scrollHeight;
|
|
2477
|
-
const currentScroll = container.scrollTop;
|
|
2478
|
-
const clientHeight = container.clientHeight;
|
|
2479
|
-
|
|
2480
|
-
// Check if we're already at the bottom
|
|
2481
|
-
if (currentScroll + clientHeight >= currentHeight - 1) {
|
|
2482
|
-
return;
|
|
2483
|
-
}
|
|
2416
|
+
return () => {
|
|
2417
|
+
container.removeEventListener('scroll', handleScroll);
|
|
2418
|
+
};
|
|
2419
|
+
}, [handleScroll, stickToBottom]);
|
|
2420
|
+
const scrollToBottom = useCallback(
|
|
2421
|
+
(behavior: ScrollBehavior = 'smooth') => {
|
|
2422
|
+
const container = containerRef.current;
|
|
2423
|
+
if (!container) return;
|
|
2484
2424
|
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2425
|
+
// Reset scroll state
|
|
2426
|
+
scrollStateRef.current.isUserScrolling = false;
|
|
2427
|
+
scrollStateRef.current.isNearBottom = true;
|
|
2428
|
+
scrollStateRef.current.scrollUpCount = 0;
|
|
2489
2429
|
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2430
|
+
const performScroll = () => {
|
|
2431
|
+
// Multiple attempts to ensure we hit the bottom
|
|
2432
|
+
const attemptScroll = (attempts = 0) => {
|
|
2433
|
+
if (attempts > 5) return; // Prevent infinite loops
|
|
2494
2434
|
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
newScroll + clientHeight < newHeight - 1
|
|
2499
|
-
) {
|
|
2500
|
-
attemptScroll(attempts + 1);
|
|
2501
|
-
}
|
|
2502
|
-
}, 50);
|
|
2503
|
-
};
|
|
2435
|
+
const currentHeight = container.scrollHeight;
|
|
2436
|
+
const currentScroll = container.scrollTop;
|
|
2437
|
+
const clientHeight = container.clientHeight;
|
|
2504
2438
|
|
|
2505
|
-
|
|
2506
|
-
|
|
2439
|
+
// Check if we're already at the bottom
|
|
2440
|
+
if (currentScroll + clientHeight >= currentHeight - 1) {
|
|
2441
|
+
return;
|
|
2442
|
+
}
|
|
2507
2443
|
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
} else {
|
|
2512
|
-
// Fallback to rAF chain
|
|
2513
|
-
requestAnimationFrame(() => {
|
|
2514
|
-
requestAnimationFrame(performScroll);
|
|
2444
|
+
container.scrollTo({
|
|
2445
|
+
top: currentHeight,
|
|
2446
|
+
behavior: behavior,
|
|
2515
2447
|
});
|
|
2516
|
-
}
|
|
2517
|
-
},
|
|
2518
|
-
[]
|
|
2519
|
-
);
|
|
2520
|
-
// Auto-scroll to bottom when new content arrives
|
|
2521
|
-
// Consolidated auto-scroll effect with debouncing
|
|
2522
|
-
useEffect(() => {
|
|
2523
|
-
if (!stickToBottom || !containerRef.current) return;
|
|
2524
|
-
|
|
2525
|
-
const container = containerRef.current;
|
|
2526
|
-
const scrollState = scrollStateRef.current;
|
|
2527
2448
|
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
scrollTimeout = setTimeout(() => {
|
|
2533
|
-
if (
|
|
2534
|
-
!scrollState.isUserScrolling &&
|
|
2535
|
-
scrollState.isNearBottom
|
|
2536
|
-
) {
|
|
2537
|
-
scrollToBottom(
|
|
2538
|
-
initialScrollRef.current ? 'instant' : 'smooth'
|
|
2539
|
-
);
|
|
2540
|
-
}
|
|
2541
|
-
}, 100);
|
|
2542
|
-
};
|
|
2449
|
+
// In slow environments, check again after a short delay
|
|
2450
|
+
setTimeout(() => {
|
|
2451
|
+
const newHeight = container.scrollHeight;
|
|
2452
|
+
const newScroll = container.scrollTop;
|
|
2543
2453
|
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2454
|
+
// If height changed or we're not at bottom, try again
|
|
2455
|
+
if (
|
|
2456
|
+
newHeight !== currentHeight ||
|
|
2457
|
+
newScroll + clientHeight < newHeight - 1
|
|
2458
|
+
) {
|
|
2459
|
+
attemptScroll(attempts + 1);
|
|
2460
|
+
}
|
|
2461
|
+
}, 50);
|
|
2462
|
+
};
|
|
2550
2463
|
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
subtree: true,
|
|
2554
|
-
attributes: true,
|
|
2555
|
-
attributeFilter: ['style', 'class'], // More specific than just 'height'
|
|
2556
|
-
});
|
|
2464
|
+
attemptScroll();
|
|
2465
|
+
};
|
|
2557
2466
|
|
|
2558
|
-
//
|
|
2559
|
-
|
|
2467
|
+
// Use requestIdleCallback for better performance in slow environments
|
|
2468
|
+
if ('requestIdleCallback' in window) {
|
|
2469
|
+
requestIdleCallback(performScroll, { timeout: 100 });
|
|
2470
|
+
} else {
|
|
2471
|
+
// Fallback to rAF chain
|
|
2472
|
+
requestAnimationFrame(() => {
|
|
2473
|
+
requestAnimationFrame(performScroll);
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
},
|
|
2477
|
+
[]
|
|
2478
|
+
);
|
|
2479
|
+
// Auto-scroll to bottom when new content arrives
|
|
2480
|
+
// Consolidated auto-scroll effect with debouncing
|
|
2481
|
+
useEffect(() => {
|
|
2482
|
+
if (!stickToBottom || !containerRef.current) return;
|
|
2483
|
+
|
|
2484
|
+
const container = containerRef.current;
|
|
2485
|
+
const scrollState = scrollStateRef.current;
|
|
2486
|
+
|
|
2487
|
+
// Debounced scroll function
|
|
2488
|
+
let scrollTimeout: NodeJS.Timeout;
|
|
2489
|
+
const debouncedScrollToBottom = () => {
|
|
2490
|
+
clearTimeout(scrollTimeout);
|
|
2491
|
+
scrollTimeout = setTimeout(() => {
|
|
2560
2492
|
if (
|
|
2561
|
-
|
|
2562
|
-
|
|
2493
|
+
!scrollState.isUserScrolling &&
|
|
2494
|
+
scrollState.isNearBottom
|
|
2563
2495
|
) {
|
|
2564
|
-
|
|
2496
|
+
scrollToBottom(
|
|
2497
|
+
initialScrollRef.current ? 'instant' : 'smooth'
|
|
2498
|
+
);
|
|
2565
2499
|
}
|
|
2566
|
-
};
|
|
2500
|
+
}, 100);
|
|
2501
|
+
};
|
|
2502
|
+
|
|
2503
|
+
// Single MutationObserver for all DOM changes
|
|
2504
|
+
const observer = new MutationObserver(() => {
|
|
2505
|
+
if (!scrollState.isUserScrolling) {
|
|
2506
|
+
debouncedScrollToBottom();
|
|
2507
|
+
}
|
|
2508
|
+
});
|
|
2567
2509
|
|
|
2568
|
-
|
|
2510
|
+
observer.observe(container, {
|
|
2511
|
+
childList: true,
|
|
2512
|
+
subtree: true,
|
|
2513
|
+
attributes: true,
|
|
2514
|
+
attributeFilter: ['style', 'class'], // More specific than just 'height'
|
|
2515
|
+
});
|
|
2569
2516
|
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
} else {
|
|
2517
|
+
// Handle image loads with event delegation
|
|
2518
|
+
const handleImageLoad = (e: Event) => {
|
|
2519
|
+
if (
|
|
2520
|
+
e.target instanceof HTMLImageElement &&
|
|
2521
|
+
!scrollState.isUserScrolling
|
|
2522
|
+
) {
|
|
2577
2523
|
debouncedScrollToBottom();
|
|
2578
2524
|
}
|
|
2525
|
+
};
|
|
2579
2526
|
|
|
2580
|
-
|
|
2581
|
-
clearTimeout(scrollTimeout);
|
|
2582
|
-
observer.disconnect();
|
|
2583
|
-
container.removeEventListener('load', handleImageLoad, true);
|
|
2584
|
-
};
|
|
2585
|
-
}, [stickToBottom, arrayKeys.length, scrollToBottom]);
|
|
2586
|
-
// Create virtual state
|
|
2587
|
-
const virtualState = useMemo(() => {
|
|
2588
|
-
const store = getGlobalStore.getState();
|
|
2589
|
-
const sourceArray = store.getShadowValue(
|
|
2590
|
-
[stateKey, ...path].join('.')
|
|
2591
|
-
) as any[];
|
|
2592
|
-
const currentKeys =
|
|
2593
|
-
store.getShadowMetadata(stateKey, path)?.arrayKeys || [];
|
|
2594
|
-
|
|
2595
|
-
const slicedArray = sourceArray.slice(
|
|
2596
|
-
range.startIndex,
|
|
2597
|
-
range.endIndex + 1
|
|
2598
|
-
);
|
|
2599
|
-
const slicedIds = currentKeys.slice(
|
|
2600
|
-
range.startIndex,
|
|
2601
|
-
range.endIndex + 1
|
|
2602
|
-
);
|
|
2527
|
+
container.addEventListener('load', handleImageLoad, true);
|
|
2603
2528
|
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
});
|
|
2610
|
-
}
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2529
|
+
// Initial scroll with proper timing
|
|
2530
|
+
if (initialScrollRef.current) {
|
|
2531
|
+
// For initial load, wait for next tick to ensure DOM is ready
|
|
2532
|
+
setTimeout(() => {
|
|
2533
|
+
scrollToBottom('instant');
|
|
2534
|
+
}, 0);
|
|
2535
|
+
} else {
|
|
2536
|
+
debouncedScrollToBottom();
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
return () => {
|
|
2540
|
+
clearTimeout(scrollTimeout);
|
|
2541
|
+
observer.disconnect();
|
|
2542
|
+
container.removeEventListener('load', handleImageLoad, true);
|
|
2543
|
+
};
|
|
2544
|
+
}, [stickToBottom, arrayKeys.length, scrollToBottom]);
|
|
2545
|
+
// Create virtual state
|
|
2546
|
+
const virtualState = useMemo(() => {
|
|
2547
|
+
const store = getGlobalStore.getState();
|
|
2548
|
+
const sourceArray = store.getShadowValue(
|
|
2549
|
+
[stateKey, ...path].join('.')
|
|
2550
|
+
) as any[];
|
|
2551
|
+
const currentKeys =
|
|
2552
|
+
store.getShadowMetadata(stateKey, path)?.arrayKeys || [];
|
|
2553
|
+
|
|
2554
|
+
const slicedArray = sourceArray.slice(
|
|
2555
|
+
range.startIndex,
|
|
2556
|
+
range.endIndex + 1
|
|
2557
|
+
);
|
|
2558
|
+
const slicedIds = currentKeys.slice(
|
|
2559
|
+
range.startIndex,
|
|
2560
|
+
range.endIndex + 1
|
|
2561
|
+
);
|
|
2562
|
+
|
|
2563
|
+
return rebuildStateShape({
|
|
2564
|
+
path,
|
|
2565
|
+
componentId: componentId!,
|
|
2566
|
+
meta: { ...meta, validIds: slicedIds },
|
|
2567
|
+
});
|
|
2568
|
+
}, [range.startIndex, range.endIndex, arrayKeys.length]);
|
|
2569
|
+
|
|
2570
|
+
return {
|
|
2571
|
+
virtualState,
|
|
2572
|
+
virtualizerProps: {
|
|
2573
|
+
outer: {
|
|
2574
|
+
ref: containerRef,
|
|
2575
|
+
style: {
|
|
2576
|
+
overflowY: 'auto',
|
|
2577
|
+
height: '100%',
|
|
2578
|
+
position: 'relative',
|
|
2628
2579
|
},
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
)?.offset || 0
|
|
2635
|
-
}px)`,
|
|
2636
|
-
},
|
|
2580
|
+
},
|
|
2581
|
+
inner: {
|
|
2582
|
+
style: {
|
|
2583
|
+
height: `${totalHeight}px`,
|
|
2584
|
+
position: 'relative',
|
|
2637
2585
|
},
|
|
2638
2586
|
},
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
measurementCache.current.get(arrayKeys[index]!)?.offset ||
|
|
2647
|
-
0;
|
|
2648
|
-
containerRef.current.scrollTo({ top: offset, behavior });
|
|
2649
|
-
}
|
|
2587
|
+
list: {
|
|
2588
|
+
style: {
|
|
2589
|
+
transform: `translateY(${
|
|
2590
|
+
measurementCache.current.get(arrayKeys[range.startIndex]!)
|
|
2591
|
+
?.offset || 0
|
|
2592
|
+
}px)`,
|
|
2593
|
+
},
|
|
2650
2594
|
},
|
|
2651
|
-
}
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
if (prop === 'stateMap') {
|
|
2655
|
-
return (
|
|
2656
|
-
callbackfn: (
|
|
2657
|
-
setter: any,
|
|
2595
|
+
},
|
|
2596
|
+
scrollToBottom,
|
|
2597
|
+
scrollToIndex: (
|
|
2658
2598
|
index: number,
|
|
2599
|
+
behavior: ScrollBehavior = 'smooth'
|
|
2600
|
+
) => {
|
|
2601
|
+
if (containerRef.current && arrayKeys[index]) {
|
|
2602
|
+
const offset =
|
|
2603
|
+
measurementCache.current.get(arrayKeys[index]!)?.offset ||
|
|
2604
|
+
0;
|
|
2605
|
+
containerRef.current.scrollTo({ top: offset, behavior });
|
|
2606
|
+
}
|
|
2607
|
+
},
|
|
2608
|
+
};
|
|
2609
|
+
};
|
|
2610
|
+
}
|
|
2611
|
+
if (prop === 'stateMap') {
|
|
2612
|
+
return (
|
|
2613
|
+
callbackfn: (
|
|
2614
|
+
setter: any,
|
|
2615
|
+
index: number,
|
|
2616
|
+
|
|
2617
|
+
arraySetter: any
|
|
2618
|
+
) => void
|
|
2619
|
+
) => {
|
|
2620
|
+
const [arrayKeys, setArrayKeys] = useState<any>(
|
|
2621
|
+
meta?.validIds ??
|
|
2622
|
+
getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
2623
|
+
?.arrayKeys
|
|
2624
|
+
);
|
|
2625
|
+
// getGlobalStore.getState().subscribeToPath(stateKeyPathKey, () => {
|
|
2626
|
+
// console.log(
|
|
2627
|
+
// "stateKeyPathKeyccccccccccccccccc",
|
|
2628
|
+
// stateKeyPathKey
|
|
2629
|
+
// );
|
|
2630
|
+
// setArrayKeys(
|
|
2631
|
+
// getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
2632
|
+
// );
|
|
2633
|
+
// });
|
|
2634
|
+
|
|
2635
|
+
const shadowValue = getGlobalStore
|
|
2636
|
+
.getState()
|
|
2637
|
+
.getShadowValue(stateKeyPathKey, meta?.validIds) as any[];
|
|
2638
|
+
if (!arrayKeys) {
|
|
2639
|
+
throw new Error('No array keys found for mapping');
|
|
2640
|
+
}
|
|
2641
|
+
const arraySetter = rebuildStateShape({
|
|
2642
|
+
path,
|
|
2643
|
+
componentId: componentId!,
|
|
2644
|
+
meta,
|
|
2645
|
+
});
|
|
2659
2646
|
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
meta?.validIds ??
|
|
2665
|
-
getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
2666
|
-
?.arrayKeys
|
|
2667
|
-
);
|
|
2668
|
-
// getGlobalStore.getState().subscribeToPath(stateKeyPathKey, () => {
|
|
2669
|
-
// console.log(
|
|
2670
|
-
// "stateKeyPathKeyccccccccccccccccc",
|
|
2671
|
-
// stateKeyPathKey
|
|
2672
|
-
// );
|
|
2673
|
-
// setArrayKeys(
|
|
2674
|
-
// getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
2675
|
-
// );
|
|
2676
|
-
// });
|
|
2677
|
-
|
|
2678
|
-
const shadowValue = getGlobalStore
|
|
2679
|
-
.getState()
|
|
2680
|
-
.getShadowValue(stateKeyPathKey, meta?.validIds) as any[];
|
|
2681
|
-
if (!arrayKeys) {
|
|
2682
|
-
throw new Error('No array keys found for mapping');
|
|
2683
|
-
}
|
|
2684
|
-
const arraySetter = rebuildStateShape({
|
|
2685
|
-
currentState: shadowValue as any,
|
|
2686
|
-
path,
|
|
2647
|
+
return shadowValue.map((item, index) => {
|
|
2648
|
+
const itemPath = arrayKeys[index]?.split('.').slice(1);
|
|
2649
|
+
const itemSetter = rebuildStateShape({
|
|
2650
|
+
path: itemPath as any,
|
|
2687
2651
|
componentId: componentId!,
|
|
2688
2652
|
meta,
|
|
2689
2653
|
});
|
|
2690
2654
|
|
|
2691
|
-
return
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
currentState: item,
|
|
2695
|
-
path: itemPath as any,
|
|
2696
|
-
componentId: componentId!,
|
|
2697
|
-
meta,
|
|
2698
|
-
});
|
|
2699
|
-
|
|
2700
|
-
return callbackfn(
|
|
2701
|
-
itemSetter,
|
|
2702
|
-
index,
|
|
2655
|
+
return callbackfn(
|
|
2656
|
+
itemSetter,
|
|
2657
|
+
index,
|
|
2703
2658
|
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2659
|
+
arraySetter
|
|
2660
|
+
);
|
|
2661
|
+
});
|
|
2662
|
+
};
|
|
2663
|
+
}
|
|
2709
2664
|
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2665
|
+
if (prop === '$stateMap') {
|
|
2666
|
+
return (callbackfn: any) =>
|
|
2667
|
+
createElement(SignalMapRenderer, {
|
|
2668
|
+
proxy: {
|
|
2669
|
+
_stateKey: stateKey,
|
|
2670
|
+
_path: path,
|
|
2671
|
+
_mapFn: callbackfn,
|
|
2672
|
+
_meta: meta,
|
|
2673
|
+
},
|
|
2674
|
+
rebuildStateShape,
|
|
2675
|
+
});
|
|
2676
|
+
} // In createProxyHandler -> handler -> get -> if (Array.isArray(currentState))
|
|
2677
|
+
|
|
2678
|
+
if (prop === 'stateFind') {
|
|
2679
|
+
return (
|
|
2680
|
+
callbackfn: (value: any, index: number) => boolean
|
|
2681
|
+
): StateObject<any> | undefined => {
|
|
2682
|
+
// 1. Use the correct set of keys: filtered/sorted from meta, or all keys from the store.
|
|
2683
|
+
const arrayKeys =
|
|
2684
|
+
meta?.validIds ??
|
|
2685
|
+
getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
2686
|
+
?.arrayKeys;
|
|
2687
|
+
|
|
2688
|
+
if (!arrayKeys) {
|
|
2689
|
+
return undefined;
|
|
2690
|
+
}
|
|
2722
2691
|
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
// 1. Use the correct set of keys: filtered/sorted from meta, or all keys from the store.
|
|
2728
|
-
const arrayKeys =
|
|
2729
|
-
meta?.validIds ??
|
|
2730
|
-
getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
2731
|
-
?.arrayKeys;
|
|
2692
|
+
// 2. Iterate through the keys, get the value for each, and run the callback.
|
|
2693
|
+
for (let i = 0; i < arrayKeys.length; i++) {
|
|
2694
|
+
const itemKey = arrayKeys[i];
|
|
2695
|
+
if (!itemKey) continue; // Safety check
|
|
2732
2696
|
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2697
|
+
const itemValue = getGlobalStore
|
|
2698
|
+
.getState()
|
|
2699
|
+
.getShadowValue(itemKey);
|
|
2736
2700
|
|
|
2737
|
-
//
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2701
|
+
// 3. If the callback returns true, we've found our item.
|
|
2702
|
+
if (callbackfn(itemValue, i)) {
|
|
2703
|
+
// Get the item's path relative to the stateKey (e.g., ['messages', '42'] -> ['42'])
|
|
2704
|
+
const itemPath = itemKey.split('.').slice(1);
|
|
2741
2705
|
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
// Get the item's path relative to the stateKey (e.g., ['messages', '42'] -> ['42'])
|
|
2749
|
-
const itemPath = itemKey.split('.').slice(1);
|
|
2750
|
-
|
|
2751
|
-
// 4. Rebuild a new, fully functional StateObject for just that item and return it.
|
|
2752
|
-
return rebuildStateShape({
|
|
2753
|
-
currentState: itemValue,
|
|
2754
|
-
path: itemPath,
|
|
2755
|
-
componentId: componentId,
|
|
2756
|
-
meta, // Pass along meta for potential further chaining
|
|
2757
|
-
});
|
|
2758
|
-
}
|
|
2706
|
+
// 4. Rebuild a new, fully functional StateObject for just that item and return it.
|
|
2707
|
+
return rebuildStateShape({
|
|
2708
|
+
path: itemPath,
|
|
2709
|
+
componentId: componentId,
|
|
2710
|
+
meta, // Pass along meta for potential further chaining
|
|
2711
|
+
});
|
|
2759
2712
|
}
|
|
2713
|
+
}
|
|
2760
2714
|
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2715
|
+
// 5. If the loop finishes without finding anything, return undefined.
|
|
2716
|
+
return undefined;
|
|
2717
|
+
};
|
|
2718
|
+
}
|
|
2719
|
+
if (prop === 'stateFilter') {
|
|
2720
|
+
return (callbackfn: (value: any, index: number) => boolean) => {
|
|
2721
|
+
const currentState = getGlobalStore
|
|
2722
|
+
.getState()
|
|
2723
|
+
.getShadowValue([stateKey, ...path].join('.'), meta?.validIds);
|
|
2724
|
+
if (!Array.isArray(currentState)) return [];
|
|
2725
|
+
const arrayKeys =
|
|
2726
|
+
meta?.validIds ??
|
|
2727
|
+
getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
2728
|
+
?.arrayKeys;
|
|
2729
|
+
|
|
2730
|
+
if (!arrayKeys) {
|
|
2731
|
+
throw new Error('No array keys found for filtering.');
|
|
2732
|
+
}
|
|
2775
2733
|
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
}
|
|
2784
|
-
return false;
|
|
2734
|
+
const newValidIds: string[] = [];
|
|
2735
|
+
const filteredArray = currentState.filter(
|
|
2736
|
+
(val: any, index: number) => {
|
|
2737
|
+
const didPass = callbackfn(val, index);
|
|
2738
|
+
if (didPass) {
|
|
2739
|
+
newValidIds.push(arrayKeys[index]!);
|
|
2740
|
+
return true;
|
|
2785
2741
|
}
|
|
2786
|
-
|
|
2742
|
+
return false;
|
|
2743
|
+
}
|
|
2744
|
+
);
|
|
2787
2745
|
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
transforms
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2746
|
+
return rebuildStateShape({
|
|
2747
|
+
path,
|
|
2748
|
+
componentId: componentId!,
|
|
2749
|
+
meta: {
|
|
2750
|
+
validIds: newValidIds,
|
|
2751
|
+
transforms: [
|
|
2752
|
+
...(meta?.transforms || []),
|
|
2753
|
+
{
|
|
2754
|
+
type: 'filter',
|
|
2755
|
+
fn: callbackfn,
|
|
2756
|
+
},
|
|
2757
|
+
],
|
|
2758
|
+
},
|
|
2759
|
+
});
|
|
2760
|
+
};
|
|
2761
|
+
}
|
|
2762
|
+
if (prop === 'stateSort') {
|
|
2763
|
+
return (compareFn: (a: any, b: any) => number) => {
|
|
2764
|
+
const currentState = getGlobalStore
|
|
2765
|
+
.getState()
|
|
2766
|
+
.getShadowValue([stateKey, ...path].join('.'), meta?.validIds);
|
|
2767
|
+
if (!Array.isArray(currentState)) return []; // Guard clause
|
|
2768
|
+
const arrayKeys =
|
|
2769
|
+
meta?.validIds ??
|
|
2770
|
+
getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
2771
|
+
?.arrayKeys;
|
|
2772
|
+
if (!arrayKeys) {
|
|
2773
|
+
throw new Error('No array keys found for sorting');
|
|
2774
|
+
}
|
|
2775
|
+
const itemsWithIds = currentState.map((item, index) => ({
|
|
2776
|
+
item,
|
|
2777
|
+
key: arrayKeys[index],
|
|
2778
|
+
}));
|
|
2779
|
+
|
|
2780
|
+
itemsWithIds
|
|
2781
|
+
.sort((a, b) => compareFn(a.item, b.item))
|
|
2782
|
+
.filter(Boolean);
|
|
2783
|
+
|
|
2784
|
+
return rebuildStateShape({
|
|
2785
|
+
path,
|
|
2786
|
+
componentId: componentId!,
|
|
2787
|
+
meta: {
|
|
2788
|
+
validIds: itemsWithIds.map((i) => i.key) as string[],
|
|
2789
|
+
transforms: [
|
|
2790
|
+
...(meta?.transforms || []),
|
|
2791
|
+
{ type: 'sort', fn: compareFn },
|
|
2792
|
+
],
|
|
2793
|
+
},
|
|
2794
|
+
});
|
|
2795
|
+
};
|
|
2796
|
+
}
|
|
2797
|
+
// In createProxyHandler, inside the get trap where you have other array methods:
|
|
2798
|
+
if (prop === 'stream') {
|
|
2799
|
+
return function <U = InferArrayElement<T>, R = U>(
|
|
2800
|
+
options: StreamOptions<U, R> = {}
|
|
2801
|
+
): StreamHandle<U> {
|
|
2802
|
+
const {
|
|
2803
|
+
bufferSize = 100,
|
|
2804
|
+
flushInterval = 100,
|
|
2805
|
+
bufferStrategy = 'accumulate',
|
|
2806
|
+
store,
|
|
2807
|
+
onFlush,
|
|
2808
|
+
} = options;
|
|
2809
|
+
|
|
2810
|
+
let buffer: U[] = [];
|
|
2811
|
+
let isPaused = false;
|
|
2812
|
+
let flushTimer: NodeJS.Timeout | null = null;
|
|
2813
|
+
|
|
2814
|
+
const addToBuffer = (item: U) => {
|
|
2815
|
+
if (isPaused) return;
|
|
2816
|
+
|
|
2817
|
+
if (bufferStrategy === 'sliding' && buffer.length >= bufferSize) {
|
|
2818
|
+
buffer.shift();
|
|
2819
|
+
} else if (
|
|
2820
|
+
bufferStrategy === 'dropping' &&
|
|
2821
|
+
buffer.length >= bufferSize
|
|
2822
|
+
) {
|
|
2823
|
+
return;
|
|
2813
2824
|
}
|
|
2814
|
-
const itemsWithIds = currentState.map((item, index) => ({
|
|
2815
|
-
item,
|
|
2816
|
-
key: arrayKeys[index],
|
|
2817
|
-
}));
|
|
2818
2825
|
|
|
2819
|
-
|
|
2820
|
-
.sort((a, b) => compareFn(a.item, b.item))
|
|
2821
|
-
.filter(Boolean);
|
|
2826
|
+
buffer.push(item);
|
|
2822
2827
|
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
componentId: componentId!,
|
|
2827
|
-
meta: {
|
|
2828
|
-
validIds: itemsWithIds.map((i) => i.key) as string[],
|
|
2829
|
-
transforms: [
|
|
2830
|
-
...(meta?.transforms || []),
|
|
2831
|
-
{ type: 'sort', fn: compareFn },
|
|
2832
|
-
],
|
|
2833
|
-
},
|
|
2834
|
-
});
|
|
2828
|
+
if (buffer.length >= bufferSize) {
|
|
2829
|
+
flushBuffer();
|
|
2830
|
+
}
|
|
2835
2831
|
};
|
|
2836
|
-
}
|
|
2837
|
-
// In createProxyHandler, inside the get trap where you have other array methods:
|
|
2838
|
-
if (prop === 'stream') {
|
|
2839
|
-
return function <U = InferArrayElement<T>, R = U>(
|
|
2840
|
-
options: StreamOptions<U, R> = {}
|
|
2841
|
-
): StreamHandle<U> {
|
|
2842
|
-
const {
|
|
2843
|
-
bufferSize = 100,
|
|
2844
|
-
flushInterval = 100,
|
|
2845
|
-
bufferStrategy = 'accumulate',
|
|
2846
|
-
store,
|
|
2847
|
-
onFlush,
|
|
2848
|
-
} = options;
|
|
2849
|
-
|
|
2850
|
-
let buffer: U[] = [];
|
|
2851
|
-
let isPaused = false;
|
|
2852
|
-
let flushTimer: NodeJS.Timeout | null = null;
|
|
2853
|
-
|
|
2854
|
-
const addToBuffer = (item: U) => {
|
|
2855
|
-
if (isPaused) return;
|
|
2856
|
-
|
|
2857
|
-
if (
|
|
2858
|
-
bufferStrategy === 'sliding' &&
|
|
2859
|
-
buffer.length >= bufferSize
|
|
2860
|
-
) {
|
|
2861
|
-
buffer.shift();
|
|
2862
|
-
} else if (
|
|
2863
|
-
bufferStrategy === 'dropping' &&
|
|
2864
|
-
buffer.length >= bufferSize
|
|
2865
|
-
) {
|
|
2866
|
-
return;
|
|
2867
|
-
}
|
|
2868
2832
|
|
|
2869
|
-
|
|
2833
|
+
const flushBuffer = () => {
|
|
2834
|
+
if (buffer.length === 0) return;
|
|
2870
2835
|
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
}
|
|
2874
|
-
};
|
|
2875
|
-
|
|
2876
|
-
const flushBuffer = () => {
|
|
2877
|
-
if (buffer.length === 0) return;
|
|
2836
|
+
const toFlush = [...buffer];
|
|
2837
|
+
buffer = [];
|
|
2878
2838
|
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
if (result !== undefined) {
|
|
2885
|
-
const items = Array.isArray(result) ? result : [result];
|
|
2886
|
-
items.forEach((item) => {
|
|
2887
|
-
effectiveSetState(item as any, path, {
|
|
2888
|
-
updateType: 'insert',
|
|
2889
|
-
});
|
|
2890
|
-
});
|
|
2891
|
-
}
|
|
2892
|
-
} else {
|
|
2893
|
-
toFlush.forEach((item) => {
|
|
2839
|
+
if (store) {
|
|
2840
|
+
const result = store(toFlush);
|
|
2841
|
+
if (result !== undefined) {
|
|
2842
|
+
const items = Array.isArray(result) ? result : [result];
|
|
2843
|
+
items.forEach((item) => {
|
|
2894
2844
|
effectiveSetState(item as any, path, {
|
|
2895
2845
|
updateType: 'insert',
|
|
2896
2846
|
});
|
|
2897
2847
|
});
|
|
2898
2848
|
}
|
|
2849
|
+
} else {
|
|
2850
|
+
toFlush.forEach((item) => {
|
|
2851
|
+
effectiveSetState(item as any, path, {
|
|
2852
|
+
updateType: 'insert',
|
|
2853
|
+
});
|
|
2854
|
+
});
|
|
2855
|
+
}
|
|
2899
2856
|
|
|
2900
|
-
|
|
2901
|
-
|
|
2857
|
+
onFlush?.(toFlush);
|
|
2858
|
+
};
|
|
2902
2859
|
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2860
|
+
if (flushInterval > 0) {
|
|
2861
|
+
flushTimer = setInterval(flushBuffer, flushInterval);
|
|
2862
|
+
}
|
|
2906
2863
|
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
streams.set(streamId, { buffer, flushTimer });
|
|
2864
|
+
const streamId = uuidv4();
|
|
2865
|
+
const currentMeta =
|
|
2866
|
+
getGlobalStore.getState().getShadowMetadata(stateKey, path) || {};
|
|
2867
|
+
const streams = currentMeta.streams || new Map();
|
|
2868
|
+
streams.set(streamId, { buffer, flushTimer });
|
|
2913
2869
|
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2870
|
+
getGlobalStore.getState().setShadowMetadata(stateKey, path, {
|
|
2871
|
+
...currentMeta,
|
|
2872
|
+
streams,
|
|
2873
|
+
});
|
|
2918
2874
|
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2875
|
+
return {
|
|
2876
|
+
write: (data: U) => addToBuffer(data),
|
|
2877
|
+
writeMany: (data: U[]) => data.forEach(addToBuffer),
|
|
2878
|
+
flush: () => flushBuffer(),
|
|
2879
|
+
pause: () => {
|
|
2880
|
+
isPaused = true;
|
|
2881
|
+
},
|
|
2882
|
+
resume: () => {
|
|
2883
|
+
isPaused = false;
|
|
2884
|
+
if (buffer.length > 0) flushBuffer();
|
|
2885
|
+
},
|
|
2886
|
+
close: () => {
|
|
2887
|
+
flushBuffer();
|
|
2888
|
+
if (flushTimer) clearInterval(flushTimer);
|
|
2933
2889
|
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
};
|
|
2890
|
+
const meta = getGlobalStore
|
|
2891
|
+
.getState()
|
|
2892
|
+
.getShadowMetadata(stateKey, path);
|
|
2893
|
+
if (meta?.streams) {
|
|
2894
|
+
meta.streams.delete(streamId);
|
|
2895
|
+
}
|
|
2896
|
+
},
|
|
2942
2897
|
};
|
|
2943
|
-
}
|
|
2898
|
+
};
|
|
2899
|
+
}
|
|
2944
2900
|
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2901
|
+
if (prop === 'stateList') {
|
|
2902
|
+
return (
|
|
2903
|
+
callbackfn: (
|
|
2904
|
+
setter: any,
|
|
2905
|
+
index: number,
|
|
2906
|
+
arraySetter: any
|
|
2907
|
+
) => ReactNode
|
|
2908
|
+
) => {
|
|
2909
|
+
const StateListWrapper = () => {
|
|
2910
|
+
const componentIdsRef = useRef<Map<string, string>>(new Map());
|
|
2911
|
+
|
|
2912
|
+
const cacheKey =
|
|
2913
|
+
meta?.transforms && meta.transforms.length > 0
|
|
2914
|
+
? `${componentId}-${hashTransforms(meta.transforms)}`
|
|
2915
|
+
: `${componentId}-base`;
|
|
2916
|
+
|
|
2917
|
+
const [updateTrigger, forceUpdate] = useState({});
|
|
2918
|
+
|
|
2919
|
+
const { validIds, arrayValues } = useMemo(() => {
|
|
2920
|
+
const cached = getGlobalStore
|
|
2921
|
+
.getState()
|
|
2922
|
+
.getShadowMetadata(stateKey, path)
|
|
2923
|
+
?.transformCaches?.get(cacheKey);
|
|
2955
2924
|
|
|
2956
|
-
|
|
2957
|
-
meta?.transforms && meta.transforms.length > 0
|
|
2958
|
-
? `${componentId}-${hashTransforms(meta.transforms)}`
|
|
2959
|
-
: `${componentId}-base`;
|
|
2925
|
+
let freshValidIds: string[];
|
|
2960
2926
|
|
|
2961
|
-
|
|
2927
|
+
if (cached && cached.validIds) {
|
|
2928
|
+
freshValidIds = cached.validIds;
|
|
2929
|
+
} else {
|
|
2930
|
+
freshValidIds = applyTransforms(
|
|
2931
|
+
stateKey,
|
|
2932
|
+
path,
|
|
2933
|
+
meta?.transforms
|
|
2934
|
+
);
|
|
2962
2935
|
|
|
2963
|
-
|
|
2964
|
-
const cached = getGlobalStore
|
|
2936
|
+
getGlobalStore
|
|
2965
2937
|
.getState()
|
|
2966
|
-
.
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
freshValidIds = cached.validIds;
|
|
2973
|
-
} else {
|
|
2974
|
-
freshValidIds = applyTransforms(
|
|
2975
|
-
stateKey,
|
|
2976
|
-
path,
|
|
2977
|
-
meta?.transforms
|
|
2978
|
-
);
|
|
2979
|
-
|
|
2980
|
-
getGlobalStore
|
|
2981
|
-
.getState()
|
|
2982
|
-
.setTransformCache(stateKey, path, cacheKey, {
|
|
2983
|
-
validIds: freshValidIds,
|
|
2984
|
-
computedAt: Date.now(),
|
|
2985
|
-
transforms: meta?.transforms || [],
|
|
2986
|
-
});
|
|
2987
|
-
}
|
|
2938
|
+
.setTransformCache(stateKey, path, cacheKey, {
|
|
2939
|
+
validIds: freshValidIds,
|
|
2940
|
+
computedAt: Date.now(),
|
|
2941
|
+
transforms: meta?.transforms || [],
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2988
2944
|
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2945
|
+
const freshValues = getGlobalStore
|
|
2946
|
+
.getState()
|
|
2947
|
+
.getShadowValue(stateKeyPathKey, freshValidIds);
|
|
2992
2948
|
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2949
|
+
return {
|
|
2950
|
+
validIds: freshValidIds,
|
|
2951
|
+
arrayValues: freshValues || [],
|
|
2952
|
+
};
|
|
2953
|
+
}, [cacheKey, updateTrigger]);
|
|
2998
2954
|
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
2955
|
+
useEffect(() => {
|
|
2956
|
+
const unsubscribe = getGlobalStore
|
|
2957
|
+
.getState()
|
|
2958
|
+
.subscribeToPath(stateKeyPathKey, (e) => {
|
|
2959
|
+
// A data change has occurred for the source array.
|
|
3004
2960
|
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
}
|
|
2961
|
+
if (e.type === 'GET_SELECTED') {
|
|
2962
|
+
return;
|
|
2963
|
+
}
|
|
2964
|
+
const shadowMeta = getGlobalStore
|
|
2965
|
+
.getState()
|
|
2966
|
+
.getShadowMetadata(stateKey, path);
|
|
2967
|
+
|
|
2968
|
+
const caches = shadowMeta?.transformCaches;
|
|
2969
|
+
if (caches) {
|
|
2970
|
+
// Iterate over ALL keys in the cache map.
|
|
2971
|
+
for (const key of caches.keys()) {
|
|
2972
|
+
// If the key belongs to this component instance, delete it.
|
|
2973
|
+
// This purges caches for 'sort by name', 'sort by score', etc.
|
|
2974
|
+
if (key.startsWith(componentId)) {
|
|
2975
|
+
caches.delete(key);
|
|
3021
2976
|
}
|
|
3022
2977
|
}
|
|
2978
|
+
}
|
|
3023
2979
|
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
return () => {
|
|
3034
|
-
unsubscribe();
|
|
3035
|
-
};
|
|
2980
|
+
if (
|
|
2981
|
+
e.type === 'INSERT' ||
|
|
2982
|
+
e.type === 'REMOVE' ||
|
|
2983
|
+
e.type === 'CLEAR_SELECTION'
|
|
2984
|
+
) {
|
|
2985
|
+
forceUpdate({});
|
|
2986
|
+
}
|
|
2987
|
+
});
|
|
3036
2988
|
|
|
3037
|
-
|
|
3038
|
-
|
|
2989
|
+
return () => {
|
|
2990
|
+
unsubscribe();
|
|
2991
|
+
};
|
|
3039
2992
|
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
}
|
|
2993
|
+
// This effect's logic now depends on the componentId to perform the purge.
|
|
2994
|
+
}, [componentId, stateKeyPathKey]);
|
|
3043
2995
|
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
componentId: componentId!,
|
|
3048
|
-
meta: {
|
|
3049
|
-
...meta,
|
|
3050
|
-
validIds: validIds,
|
|
3051
|
-
},
|
|
3052
|
-
});
|
|
2996
|
+
if (!Array.isArray(arrayValues)) {
|
|
2997
|
+
return null;
|
|
2998
|
+
}
|
|
3053
2999
|
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3000
|
+
const arraySetter = rebuildStateShape({
|
|
3001
|
+
path,
|
|
3002
|
+
componentId: componentId!,
|
|
3003
|
+
meta: {
|
|
3004
|
+
...meta,
|
|
3005
|
+
validIds: validIds,
|
|
3006
|
+
},
|
|
3007
|
+
});
|
|
3058
3008
|
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3009
|
+
return (
|
|
3010
|
+
<>
|
|
3011
|
+
{arrayValues.map((item, localIndex) => {
|
|
3012
|
+
const itemKey = validIds[localIndex];
|
|
3062
3013
|
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
itemComponentId = uuidv4();
|
|
3067
|
-
componentIdsRef.current.set(itemKey, itemComponentId);
|
|
3068
|
-
}
|
|
3014
|
+
if (!itemKey) {
|
|
3015
|
+
return null;
|
|
3016
|
+
}
|
|
3069
3017
|
|
|
3070
|
-
|
|
3018
|
+
let itemComponentId = componentIdsRef.current.get(itemKey);
|
|
3019
|
+
if (!itemComponentId) {
|
|
3020
|
+
itemComponentId = uuidv4();
|
|
3021
|
+
componentIdsRef.current.set(itemKey, itemComponentId);
|
|
3022
|
+
}
|
|
3071
3023
|
|
|
3072
|
-
|
|
3073
|
-
key: itemKey,
|
|
3074
|
-
stateKey,
|
|
3075
|
-
itemComponentId,
|
|
3076
|
-
itemPath,
|
|
3077
|
-
localIndex,
|
|
3078
|
-
arraySetter,
|
|
3079
|
-
rebuildStateShape,
|
|
3080
|
-
renderFn: callbackfn,
|
|
3081
|
-
});
|
|
3082
|
-
})}
|
|
3083
|
-
</>
|
|
3084
|
-
);
|
|
3085
|
-
};
|
|
3024
|
+
const itemPath = itemKey.split('.').slice(1);
|
|
3086
3025
|
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3026
|
+
return createElement(MemoizedCogsItemWrapper, {
|
|
3027
|
+
key: itemKey,
|
|
3028
|
+
stateKey,
|
|
3029
|
+
itemComponentId,
|
|
3030
|
+
itemPath,
|
|
3031
|
+
localIndex,
|
|
3032
|
+
arraySetter,
|
|
3033
|
+
rebuildStateShape,
|
|
3034
|
+
renderFn: callbackfn,
|
|
3035
|
+
});
|
|
3036
|
+
})}
|
|
3037
|
+
</>
|
|
3097
3038
|
);
|
|
3098
|
-
return rebuildStateShape({
|
|
3099
|
-
currentState: flattenedResults as any,
|
|
3100
|
-
path: [...path, '[*]', fieldName],
|
|
3101
|
-
componentId: componentId!,
|
|
3102
|
-
meta,
|
|
3103
|
-
});
|
|
3104
3039
|
};
|
|
3105
|
-
}
|
|
3106
|
-
if (prop === 'index') {
|
|
3107
|
-
return (index: number) => {
|
|
3108
|
-
const arrayKeys = getGlobalStore
|
|
3109
|
-
.getState()
|
|
3110
|
-
.getShadowMetadata(stateKey, path)
|
|
3111
|
-
?.arrayKeys?.filter(
|
|
3112
|
-
(key) =>
|
|
3113
|
-
!meta?.validIds ||
|
|
3114
|
-
(meta?.validIds && meta?.validIds?.includes(key))
|
|
3115
|
-
);
|
|
3116
|
-
const itemId = arrayKeys?.[index];
|
|
3117
|
-
if (!itemId) return undefined;
|
|
3118
|
-
const value = getGlobalStore
|
|
3119
|
-
.getState()
|
|
3120
|
-
.getShadowValue(itemId, meta?.validIds);
|
|
3121
|
-
const state = rebuildStateShape({
|
|
3122
|
-
currentState: value,
|
|
3123
|
-
path: itemId.split('.').slice(1) as string[],
|
|
3124
|
-
componentId: componentId!,
|
|
3125
|
-
meta,
|
|
3126
|
-
});
|
|
3127
|
-
return state;
|
|
3128
|
-
};
|
|
3129
|
-
}
|
|
3130
|
-
if (prop === 'last') {
|
|
3131
|
-
return () => {
|
|
3132
|
-
const currentArray = getGlobalStore
|
|
3133
|
-
.getState()
|
|
3134
|
-
.getShadowValue(stateKey, path) as any[];
|
|
3135
|
-
if (currentArray.length === 0) return undefined;
|
|
3136
|
-
const lastIndex = currentArray.length - 1;
|
|
3137
|
-
const lastValue = currentArray[lastIndex];
|
|
3138
|
-
const newPath = [...path, lastIndex.toString()];
|
|
3139
|
-
return rebuildStateShape({
|
|
3140
|
-
currentState: lastValue,
|
|
3141
|
-
path: newPath,
|
|
3142
|
-
componentId: componentId!,
|
|
3143
|
-
meta,
|
|
3144
|
-
});
|
|
3145
|
-
};
|
|
3146
|
-
}
|
|
3147
|
-
if (prop === 'insert') {
|
|
3148
|
-
return (
|
|
3149
|
-
payload: InsertParams<InferArrayElement<T>>,
|
|
3150
|
-
index?: number
|
|
3151
|
-
) => {
|
|
3152
|
-
effectiveSetState(payload as any, path, { updateType: 'insert' });
|
|
3153
|
-
return rebuildStateShape({
|
|
3154
|
-
currentState: getGlobalStore
|
|
3155
|
-
.getState()
|
|
3156
|
-
.getShadowValue(stateKey, path),
|
|
3157
|
-
path,
|
|
3158
|
-
componentId: componentId!,
|
|
3159
|
-
meta,
|
|
3160
|
-
});
|
|
3161
|
-
};
|
|
3162
|
-
}
|
|
3163
|
-
if (prop === 'uniqueInsert') {
|
|
3164
|
-
return (
|
|
3165
|
-
payload: UpdateArg<T>,
|
|
3166
|
-
fields?: (keyof InferArrayElement<T>)[],
|
|
3167
|
-
onMatch?: (existingItem: any) => any
|
|
3168
|
-
) => {
|
|
3169
|
-
const currentArray = getGlobalStore
|
|
3170
|
-
.getState()
|
|
3171
|
-
.getShadowValue(stateKey, path) as any[];
|
|
3172
|
-
const newValue = isFunction<T>(payload)
|
|
3173
|
-
? payload(currentArray as any)
|
|
3174
|
-
: (payload as any);
|
|
3175
|
-
|
|
3176
|
-
let matchedItem: any = null;
|
|
3177
|
-
const isUnique = !currentArray.some((item) => {
|
|
3178
|
-
const isMatch = fields
|
|
3179
|
-
? fields.every((field) =>
|
|
3180
|
-
isDeepEqual(item[field], newValue[field])
|
|
3181
|
-
)
|
|
3182
|
-
: isDeepEqual(item, newValue);
|
|
3183
|
-
if (isMatch) matchedItem = item;
|
|
3184
|
-
return isMatch;
|
|
3185
|
-
});
|
|
3186
|
-
|
|
3187
|
-
if (isUnique) {
|
|
3188
|
-
invalidateCachePath(path);
|
|
3189
|
-
effectiveSetState(newValue, path, { updateType: 'insert' });
|
|
3190
|
-
} else if (onMatch && matchedItem) {
|
|
3191
|
-
const updatedItem = onMatch(matchedItem);
|
|
3192
|
-
const updatedArray = currentArray.map((item) =>
|
|
3193
|
-
isDeepEqual(item, matchedItem) ? updatedItem : item
|
|
3194
|
-
);
|
|
3195
|
-
invalidateCachePath(path);
|
|
3196
|
-
effectiveSetState(updatedArray as any, path, {
|
|
3197
|
-
updateType: 'update',
|
|
3198
|
-
});
|
|
3199
|
-
}
|
|
3200
|
-
};
|
|
3201
|
-
}
|
|
3202
3040
|
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3041
|
+
return <StateListWrapper />;
|
|
3042
|
+
};
|
|
3043
|
+
}
|
|
3044
|
+
if (prop === 'stateFlattenOn') {
|
|
3045
|
+
return (fieldName: string) => {
|
|
3046
|
+
const currentState = getGlobalStore
|
|
3047
|
+
.getState()
|
|
3048
|
+
.getShadowValue([stateKey, ...path].join('.'), meta?.validIds);
|
|
3049
|
+
if (!Array.isArray(currentState)) return []; // Guard clause
|
|
3050
|
+
const arrayToMap = currentState as any[];
|
|
3211
3051
|
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3052
|
+
stateVersion++;
|
|
3053
|
+
const flattenedResults = arrayToMap.flatMap(
|
|
3054
|
+
(val: any) => val[fieldName] ?? []
|
|
3055
|
+
);
|
|
3056
|
+
return rebuildStateShape({
|
|
3057
|
+
path: [...path, '[*]', fieldName],
|
|
3058
|
+
componentId: componentId!,
|
|
3059
|
+
meta,
|
|
3060
|
+
});
|
|
3061
|
+
};
|
|
3062
|
+
}
|
|
3063
|
+
if (prop === 'index') {
|
|
3064
|
+
return (index: number) => {
|
|
3065
|
+
const arrayKeys = getGlobalStore
|
|
3066
|
+
.getState()
|
|
3067
|
+
.getShadowMetadata(stateKey, path)
|
|
3068
|
+
?.arrayKeys?.filter(
|
|
3069
|
+
(key) =>
|
|
3070
|
+
!meta?.validIds ||
|
|
3071
|
+
(meta?.validIds && meta?.validIds?.includes(key))
|
|
3072
|
+
);
|
|
3073
|
+
const itemId = arrayKeys?.[index];
|
|
3074
|
+
if (!itemId) return undefined;
|
|
3075
|
+
const value = getGlobalStore
|
|
3076
|
+
.getState()
|
|
3077
|
+
.getShadowValue(itemId, meta?.validIds);
|
|
3078
|
+
const state = rebuildStateShape({
|
|
3079
|
+
path: itemId.split('.').slice(1) as string[],
|
|
3080
|
+
componentId: componentId!,
|
|
3081
|
+
meta,
|
|
3082
|
+
});
|
|
3083
|
+
return state;
|
|
3084
|
+
};
|
|
3085
|
+
}
|
|
3086
|
+
if (prop === 'last') {
|
|
3087
|
+
return () => {
|
|
3088
|
+
const currentArray = getGlobalStore
|
|
3089
|
+
.getState()
|
|
3090
|
+
.getShadowValue(stateKey, path) as any[];
|
|
3091
|
+
if (currentArray.length === 0) return undefined;
|
|
3092
|
+
const lastIndex = currentArray.length - 1;
|
|
3093
|
+
const lastValue = currentArray[lastIndex];
|
|
3094
|
+
const newPath = [...path, lastIndex.toString()];
|
|
3095
|
+
return rebuildStateShape({
|
|
3096
|
+
path: newPath,
|
|
3097
|
+
componentId: componentId!,
|
|
3098
|
+
meta,
|
|
3099
|
+
});
|
|
3100
|
+
};
|
|
3101
|
+
}
|
|
3102
|
+
if (prop === 'insert') {
|
|
3103
|
+
return (
|
|
3104
|
+
payload: InsertParams<InferArrayElement<T>>,
|
|
3105
|
+
index?: number
|
|
3106
|
+
) => {
|
|
3107
|
+
effectiveSetState(payload as any, path, { updateType: 'insert' });
|
|
3108
|
+
return rebuildStateShape({
|
|
3109
|
+
path,
|
|
3110
|
+
componentId: componentId!,
|
|
3111
|
+
meta,
|
|
3112
|
+
});
|
|
3113
|
+
};
|
|
3114
|
+
}
|
|
3115
|
+
if (prop === 'uniqueInsert') {
|
|
3116
|
+
return (
|
|
3117
|
+
payload: UpdateArg<T>,
|
|
3118
|
+
fields?: (keyof InferArrayElement<T>)[],
|
|
3119
|
+
onMatch?: (existingItem: any) => any
|
|
3120
|
+
) => {
|
|
3121
|
+
const currentArray = getGlobalStore
|
|
3122
|
+
.getState()
|
|
3123
|
+
.getShadowValue(stateKey, path) as any[];
|
|
3124
|
+
const newValue = isFunction<T>(payload)
|
|
3125
|
+
? payload(currentArray as any)
|
|
3126
|
+
: (payload as any);
|
|
3127
|
+
|
|
3128
|
+
let matchedItem: any = null;
|
|
3129
|
+
const isUnique = !currentArray.some((item) => {
|
|
3130
|
+
const isMatch = fields
|
|
3131
|
+
? fields.every((field) =>
|
|
3132
|
+
isDeepEqual(item[field], newValue[field])
|
|
3133
|
+
)
|
|
3134
|
+
: isDeepEqual(item, newValue);
|
|
3135
|
+
if (isMatch) matchedItem = item;
|
|
3136
|
+
return isMatch;
|
|
3137
|
+
});
|
|
3218
3138
|
|
|
3219
|
-
|
|
3220
|
-
|
|
3139
|
+
if (isUnique) {
|
|
3140
|
+
effectiveSetState(newValue, path, { updateType: 'insert' });
|
|
3141
|
+
} else if (onMatch && matchedItem) {
|
|
3142
|
+
const updatedItem = onMatch(matchedItem);
|
|
3143
|
+
const updatedArray = currentArray.map((item) =>
|
|
3144
|
+
isDeepEqual(item, matchedItem) ? updatedItem : item
|
|
3145
|
+
);
|
|
3221
3146
|
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
updateType: 'cut',
|
|
3147
|
+
effectiveSetState(updatedArray as any, path, {
|
|
3148
|
+
updateType: 'update',
|
|
3225
3149
|
});
|
|
3226
|
-
}
|
|
3227
|
-
}
|
|
3228
|
-
|
|
3229
|
-
return () => {
|
|
3230
|
-
const validKeys = applyTransforms(
|
|
3231
|
-
stateKey,
|
|
3232
|
-
path,
|
|
3233
|
-
meta?.transforms
|
|
3234
|
-
);
|
|
3150
|
+
}
|
|
3151
|
+
};
|
|
3152
|
+
}
|
|
3235
3153
|
|
|
3236
|
-
|
|
3154
|
+
if (prop === 'cut') {
|
|
3155
|
+
return (index?: number, options?: { waitForSync?: boolean }) => {
|
|
3156
|
+
const currentState = getGlobalStore
|
|
3157
|
+
.getState()
|
|
3158
|
+
.getShadowValue([stateKey, ...path].join('.'), meta?.validIds);
|
|
3159
|
+
const validKeys =
|
|
3160
|
+
meta?.validIds ??
|
|
3161
|
+
getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
3162
|
+
?.arrayKeys;
|
|
3163
|
+
|
|
3164
|
+
if (!validKeys || validKeys.length === 0) return;
|
|
3165
|
+
|
|
3166
|
+
const indexToCut =
|
|
3167
|
+
index == -1
|
|
3168
|
+
? validKeys.length - 1
|
|
3169
|
+
: index !== undefined
|
|
3170
|
+
? index
|
|
3171
|
+
: validKeys.length - 1;
|
|
3172
|
+
|
|
3173
|
+
const fullIdToCut = validKeys[indexToCut];
|
|
3174
|
+
if (!fullIdToCut) return; // Index out of bounds
|
|
3175
|
+
|
|
3176
|
+
const pathForCut = fullIdToCut.split('.').slice(1);
|
|
3177
|
+
effectiveSetState(currentState, pathForCut, {
|
|
3178
|
+
updateType: 'cut',
|
|
3179
|
+
});
|
|
3180
|
+
};
|
|
3181
|
+
}
|
|
3182
|
+
if (prop === 'cutSelected') {
|
|
3183
|
+
return () => {
|
|
3184
|
+
const validKeys = applyTransforms(stateKey, path, meta?.transforms);
|
|
3185
|
+
const currentState = getGlobalStore
|
|
3186
|
+
.getState()
|
|
3187
|
+
.getShadowValue([stateKey, ...path].join('.'), meta?.validIds);
|
|
3188
|
+
if (!validKeys || validKeys.length === 0) return;
|
|
3237
3189
|
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3190
|
+
const indexKeyToCut = getGlobalStore
|
|
3191
|
+
.getState()
|
|
3192
|
+
.selectedIndicesMap.get(stateKeyPathKey);
|
|
3241
3193
|
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3194
|
+
let indexToCut = validKeys.findIndex(
|
|
3195
|
+
(key) => key === indexKeyToCut
|
|
3196
|
+
);
|
|
3245
3197
|
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3198
|
+
const pathForCut = validKeys[
|
|
3199
|
+
indexToCut == -1 ? validKeys.length - 1 : indexToCut
|
|
3200
|
+
]
|
|
3201
|
+
?.split('.')
|
|
3202
|
+
.slice(1);
|
|
3203
|
+
getGlobalStore
|
|
3204
|
+
.getState()
|
|
3205
|
+
.clearSelectedIndex({ arrayKey: stateKeyPathKey });
|
|
3206
|
+
const parentPath = pathForCut?.slice(0, -1)!;
|
|
3207
|
+
notifySelectionComponents(stateKey, parentPath);
|
|
3208
|
+
effectiveSetState(currentState, pathForCut!, {
|
|
3209
|
+
updateType: 'cut',
|
|
3210
|
+
});
|
|
3211
|
+
};
|
|
3212
|
+
}
|
|
3213
|
+
if (prop === 'cutByValue') {
|
|
3214
|
+
return (value: string | number | boolean) => {
|
|
3215
|
+
// Step 1: Get the list of all unique keys for the current view.
|
|
3216
|
+
const arrayMeta = getGlobalStore
|
|
3217
|
+
.getState()
|
|
3218
|
+
.getShadowMetadata(stateKey, path);
|
|
3219
|
+
const relevantKeys = meta?.validIds ?? arrayMeta?.arrayKeys;
|
|
3268
3220
|
|
|
3269
|
-
|
|
3221
|
+
if (!relevantKeys) return;
|
|
3270
3222
|
|
|
3271
|
-
|
|
3223
|
+
let keyToCut: string | null = null;
|
|
3272
3224
|
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
}
|
|
3225
|
+
// Step 2: Iterate through the KEYS, get the value for each, and find the match.
|
|
3226
|
+
for (const key of relevantKeys) {
|
|
3227
|
+
const itemValue = getGlobalStore.getState().getShadowValue(key);
|
|
3228
|
+
if (itemValue === value) {
|
|
3229
|
+
keyToCut = key;
|
|
3230
|
+
break; // We found the key, no need to search further.
|
|
3280
3231
|
}
|
|
3232
|
+
}
|
|
3281
3233
|
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3234
|
+
// Step 3: If we found a matching key, use it to perform the cut.
|
|
3235
|
+
if (keyToCut) {
|
|
3236
|
+
const itemPath = keyToCut.split('.').slice(1);
|
|
3237
|
+
effectiveSetState(null as any, itemPath, { updateType: 'cut' });
|
|
3238
|
+
}
|
|
3239
|
+
};
|
|
3240
|
+
}
|
|
3289
3241
|
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3242
|
+
if (prop === 'toggleByValue') {
|
|
3243
|
+
return (value: string | number | boolean) => {
|
|
3244
|
+
// Step 1: Get the list of all unique keys for the current view.
|
|
3245
|
+
const arrayMeta = getGlobalStore
|
|
3246
|
+
.getState()
|
|
3247
|
+
.getShadowMetadata(stateKey, path);
|
|
3248
|
+
const relevantKeys = meta?.validIds ?? arrayMeta?.arrayKeys;
|
|
3297
3249
|
|
|
3298
|
-
|
|
3250
|
+
if (!relevantKeys) return;
|
|
3299
3251
|
|
|
3300
|
-
|
|
3252
|
+
let keyToCut: string | null = null;
|
|
3301
3253
|
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
}
|
|
3254
|
+
// Step 2: Iterate through the KEYS to find the one matching the value. This is the robust way.
|
|
3255
|
+
for (const key of relevantKeys) {
|
|
3256
|
+
const itemValue = getGlobalStore.getState().getShadowValue(key);
|
|
3257
|
+
console.log('itemValue sdasdasdasd', itemValue);
|
|
3258
|
+
if (itemValue === value) {
|
|
3259
|
+
keyToCut = key;
|
|
3260
|
+
break; // Found it!
|
|
3310
3261
|
}
|
|
3262
|
+
}
|
|
3263
|
+
console.log('itemValue keyToCut', keyToCut);
|
|
3264
|
+
// Step 3: Act based on whether the key was found.
|
|
3265
|
+
if (keyToCut) {
|
|
3266
|
+
// Item exists, so we CUT it using its *actual* key.
|
|
3267
|
+
const itemPath = keyToCut.split('.').slice(1);
|
|
3311
3268
|
console.log('itemValue keyToCut', keyToCut);
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
if (prop === 'findWith') {
|
|
3327
|
-
return (
|
|
3328
|
-
searchKey: keyof InferArrayElement<T>,
|
|
3329
|
-
searchValue: any
|
|
3330
|
-
) => {
|
|
3331
|
-
const arrayKeys = getGlobalStore
|
|
3332
|
-
.getState()
|
|
3333
|
-
.getShadowMetadata(stateKey, path)?.arrayKeys;
|
|
3269
|
+
effectiveSetState(value as any, itemPath, {
|
|
3270
|
+
updateType: 'cut',
|
|
3271
|
+
});
|
|
3272
|
+
} else {
|
|
3273
|
+
// Item does not exist, so we INSERT it.
|
|
3274
|
+
effectiveSetState(value as any, path, { updateType: 'insert' });
|
|
3275
|
+
}
|
|
3276
|
+
};
|
|
3277
|
+
}
|
|
3278
|
+
if (prop === 'findWith') {
|
|
3279
|
+
return (searchKey: keyof InferArrayElement<T>, searchValue: any) => {
|
|
3280
|
+
const arrayKeys = getGlobalStore
|
|
3281
|
+
.getState()
|
|
3282
|
+
.getShadowMetadata(stateKey, path)?.arrayKeys;
|
|
3334
3283
|
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3284
|
+
if (!arrayKeys) {
|
|
3285
|
+
throw new Error('No array keys found for sorting');
|
|
3286
|
+
}
|
|
3338
3287
|
|
|
3339
|
-
|
|
3340
|
-
|
|
3288
|
+
let value = null;
|
|
3289
|
+
let foundPath: string[] = [];
|
|
3341
3290
|
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
}
|
|
3291
|
+
for (const fullPath of arrayKeys) {
|
|
3292
|
+
let shadowValue = getGlobalStore
|
|
3293
|
+
.getState()
|
|
3294
|
+
.getShadowValue(fullPath, meta?.validIds);
|
|
3295
|
+
if (shadowValue && shadowValue[searchKey] === searchValue) {
|
|
3296
|
+
value = shadowValue;
|
|
3297
|
+
foundPath = fullPath.split('.').slice(1);
|
|
3298
|
+
break;
|
|
3351
3299
|
}
|
|
3300
|
+
}
|
|
3352
3301
|
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
};
|
|
3360
|
-
}
|
|
3302
|
+
return rebuildStateShape({
|
|
3303
|
+
path: foundPath,
|
|
3304
|
+
componentId: componentId!,
|
|
3305
|
+
meta,
|
|
3306
|
+
});
|
|
3307
|
+
};
|
|
3361
3308
|
}
|
|
3362
3309
|
|
|
3363
|
-
if (prop === '
|
|
3310
|
+
if (prop === 'cutThis') {
|
|
3364
3311
|
let shadowValue = getGlobalStore
|
|
3365
3312
|
.getState()
|
|
3366
3313
|
.getShadowValue(path.join('.'));
|
|
@@ -3661,29 +3608,86 @@ function createProxyHandler<T>(
|
|
|
3661
3608
|
if (prop === '_stateKey') return stateKey;
|
|
3662
3609
|
if (prop === '_path') return path;
|
|
3663
3610
|
if (prop === 'update') {
|
|
3611
|
+
if (recursionTimerName) {
|
|
3612
|
+
console.timeEnd(recursionTimerName);
|
|
3613
|
+
recursionTimerName = null;
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3664
3616
|
return (payload: UpdateArg<T>) => {
|
|
3665
|
-
//
|
|
3666
|
-
|
|
3617
|
+
// Check if we're in a React event handler
|
|
3618
|
+
const error = new Error();
|
|
3619
|
+
const stack = error.stack || '';
|
|
3620
|
+
const inReactEvent =
|
|
3621
|
+
stack.includes('onClick') ||
|
|
3622
|
+
stack.includes('dispatchEvent') ||
|
|
3623
|
+
stack.includes('batchedUpdates');
|
|
3624
|
+
|
|
3625
|
+
// Only batch if we're in a React event
|
|
3626
|
+
if (inReactEvent) {
|
|
3627
|
+
const batchKey = `${stateKey}.${path.join('.')}`;
|
|
3628
|
+
|
|
3629
|
+
// Schedule flush if not already scheduled
|
|
3630
|
+
if (!batchFlushScheduled) {
|
|
3631
|
+
updateBatchQueue.clear();
|
|
3632
|
+
batchFlushScheduled = true;
|
|
3633
|
+
|
|
3634
|
+
queueMicrotask(() => {
|
|
3635
|
+
// Process all batched updates
|
|
3636
|
+
for (const [key, updates] of updateBatchQueue) {
|
|
3637
|
+
const parts = key.split('.');
|
|
3638
|
+
const batchStateKey = parts[0];
|
|
3639
|
+
const batchPath = parts.slice(1);
|
|
3640
|
+
|
|
3641
|
+
// Compose all updates for this path
|
|
3642
|
+
const composedUpdate = updates.reduce(
|
|
3643
|
+
(composed, update) => {
|
|
3644
|
+
if (
|
|
3645
|
+
typeof update === 'function' &&
|
|
3646
|
+
typeof composed === 'function'
|
|
3647
|
+
) {
|
|
3648
|
+
// Compose functions
|
|
3649
|
+
return (state: any) => update(composed(state));
|
|
3650
|
+
}
|
|
3651
|
+
// If not functions, last one wins
|
|
3652
|
+
return update;
|
|
3653
|
+
}
|
|
3654
|
+
);
|
|
3655
|
+
|
|
3656
|
+
// Call effectiveSetState ONCE with composed update
|
|
3657
|
+
effectiveSetState(composedUpdate as any, batchPath, {
|
|
3658
|
+
updateType: 'update',
|
|
3659
|
+
});
|
|
3660
|
+
}
|
|
3661
|
+
|
|
3662
|
+
updateBatchQueue.clear();
|
|
3663
|
+
batchFlushScheduled = false;
|
|
3664
|
+
});
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
// Add to batch
|
|
3668
|
+
const existing = updateBatchQueue.get(batchKey) || [];
|
|
3669
|
+
existing.push(payload);
|
|
3670
|
+
updateBatchQueue.set(batchKey, existing);
|
|
3671
|
+
} else {
|
|
3672
|
+
// NOT in React event - execute immediately
|
|
3673
|
+
console.time('update inner');
|
|
3674
|
+
effectiveSetState(payload as any, path, { updateType: 'update' });
|
|
3675
|
+
console.timeEnd('update inner');
|
|
3676
|
+
}
|
|
3667
3677
|
|
|
3668
3678
|
return {
|
|
3669
|
-
/**
|
|
3670
|
-
* Marks this specific item, which was just updated, as 'synced' (not dirty).
|
|
3671
|
-
*/
|
|
3672
3679
|
synced: () => {
|
|
3673
|
-
// This function "remembers" the path of the item that was just updated.
|
|
3674
3680
|
const shadowMeta = getGlobalStore
|
|
3675
3681
|
.getState()
|
|
3676
3682
|
.getShadowMetadata(stateKey, path);
|
|
3677
3683
|
|
|
3678
|
-
// It updates ONLY the metadata for that specific item.
|
|
3679
3684
|
getGlobalStore.getState().setShadowMetadata(stateKey, path, {
|
|
3680
3685
|
...shadowMeta,
|
|
3681
|
-
isDirty: false,
|
|
3682
|
-
stateSource: 'server',
|
|
3683
|
-
lastServerSync: Date.now(),
|
|
3686
|
+
isDirty: false,
|
|
3687
|
+
stateSource: 'server',
|
|
3688
|
+
lastServerSync: Date.now(),
|
|
3684
3689
|
});
|
|
3685
3690
|
|
|
3686
|
-
// Force a re-render for components watching this path
|
|
3687
3691
|
const fullPath = [stateKey, ...path].join('.');
|
|
3688
3692
|
getGlobalStore.getState().notifyPathSubscribers(fullPath, {
|
|
3689
3693
|
type: 'SYNC_STATUS_CHANGE',
|
|
@@ -3697,10 +3701,9 @@ function createProxyHandler<T>(
|
|
|
3697
3701
|
if (prop === 'toggle') {
|
|
3698
3702
|
const currentValueAtPath = getGlobalStore
|
|
3699
3703
|
.getState()
|
|
3700
|
-
.getShadowValue([stateKey, ...path].join('.'));
|
|
3704
|
+
.getShadowValue([stateKey, ...path].join('.'), meta?.validIds);
|
|
3701
3705
|
|
|
3702
|
-
|
|
3703
|
-
if (typeof currentState != 'boolean') {
|
|
3706
|
+
if (typeof currentValueAtPath != 'boolean') {
|
|
3704
3707
|
throw new Error('toggle() can only be used on boolean values');
|
|
3705
3708
|
}
|
|
3706
3709
|
return () => {
|
|
@@ -3728,7 +3731,6 @@ function createProxyHandler<T>(
|
|
|
3728
3731
|
.getState()
|
|
3729
3732
|
.getShadowValue(stateKey, nextPath);
|
|
3730
3733
|
return rebuildStateShape({
|
|
3731
|
-
currentState: nextValue,
|
|
3732
3734
|
path: nextPath,
|
|
3733
3735
|
componentId: componentId!,
|
|
3734
3736
|
meta,
|
|
@@ -3737,12 +3739,11 @@ function createProxyHandler<T>(
|
|
|
3737
3739
|
};
|
|
3738
3740
|
|
|
3739
3741
|
const proxyInstance = new Proxy(baseFunction, handler);
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
stateVersion: stateVersion,
|
|
3743
|
-
});
|
|
3742
|
+
proxyCache.set(cacheKey, proxyInstance);
|
|
3743
|
+
console.timeEnd('rebuildStateShape Inner');
|
|
3744
3744
|
return proxyInstance;
|
|
3745
3745
|
}
|
|
3746
|
+
console.timeEnd('rebuildStateShape Outer');
|
|
3746
3747
|
|
|
3747
3748
|
const baseObj = {
|
|
3748
3749
|
revertToInitialState: (obj?: { validationKey?: string }) => {
|
|
@@ -3766,11 +3767,10 @@ function createProxyHandler<T>(
|
|
|
3766
3767
|
getGlobalStore.getState().initialStateGlobal[stateKey];
|
|
3767
3768
|
|
|
3768
3769
|
getGlobalStore.getState().clearSelectedIndexesForState(stateKey);
|
|
3769
|
-
|
|
3770
|
+
|
|
3770
3771
|
stateVersion++;
|
|
3771
3772
|
getGlobalStore.getState().initializeShadowState(stateKey, initialState);
|
|
3772
|
-
|
|
3773
|
-
currentState: initialState,
|
|
3773
|
+
rebuildStateShape({
|
|
3774
3774
|
path: [],
|
|
3775
3775
|
componentId: componentId!,
|
|
3776
3776
|
});
|
|
@@ -3797,7 +3797,6 @@ function createProxyHandler<T>(
|
|
|
3797
3797
|
return initialState;
|
|
3798
3798
|
},
|
|
3799
3799
|
updateInitialState: (newState: T) => {
|
|
3800
|
-
shapeCache.clear();
|
|
3801
3800
|
stateVersion++;
|
|
3802
3801
|
|
|
3803
3802
|
const newUpdaterState = createProxyHandler(
|
|
@@ -3839,7 +3838,6 @@ function createProxyHandler<T>(
|
|
|
3839
3838
|
},
|
|
3840
3839
|
};
|
|
3841
3840
|
const returnShape = rebuildStateShape({
|
|
3842
|
-
currentState: getGlobalStore.getState().getShadowValue(stateKey, []),
|
|
3843
3841
|
componentId,
|
|
3844
3842
|
path: [],
|
|
3845
3843
|
});
|